accounting workflows

This commit is contained in:
Thanakorn S
2026-05-20 10:17:02 +07:00
parent 04a683bd02
commit 7396db6ffc
199 changed files with 23539 additions and 5345 deletions
@@ -0,0 +1,140 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PurchaseOrderManager.php';
require '../../../assets/utils/classes/PurchaseRequestManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$request_id = (int)($data['request_id'] ?? 0);
$contact_id = (int)($data['contact_id'] ?? 0);
$convert_items = $data['convert_items'] ?? [];
$discount = (float)($data['discount'] ?? 0);
$shipping_fee = (float)($data['shipping_fee'] ?? 0);
if (!$request_id) {
exit(json_encode(['success' => 0, 'message' => 'Purchase request ID is required.']));
}
if (!$contact_id) {
exit(json_encode(['success' => 0, 'message' => 'Supplier (contact_id) is required for the PO.']));
}
$prm = new PurchaseRequestManager($pdo2, $company_id);
// Load PR — must be Approved
$pr = $prm->getById($request_id);
if (!$pr || (int)$pr['status'] !== 2) {
exit(json_encode(['success' => 0, 'message' => 'Purchase request not found or not in Approved status.']));
}
$pr_items = $pr['items'];
if (empty($pr_items)) {
exit(json_encode(['success' => 0, 'message' => 'Purchase request has no items.']));
}
if ((float)$pr['total_remaining'] <= 0.000001) {
exit(json_encode(['success' => 0, 'message' => 'Purchase request is already fully converted.']));
}
// Index by item_id for validation
$pr_by_id = [];
foreach ($pr_items as $pi) {
$pr_by_id[(int)$pi['item_id']] = $pi;
}
// Default to all remaining
if (empty($convert_items)) {
$convert_items = [];
foreach ($pr_items as $pi) {
$remaining = (float)$pi['remaining_qty'];
if ($remaining > 0.000001) {
$convert_items[] = ['item_id' => (int)$pi['item_id'], 'quantity' => $remaining];
}
}
}
// Validate quantities and build PO items
$po_items = [];
$validated = [];
foreach ($convert_items as $ci) {
$item_id = (int)($ci['item_id'] ?? 0);
$qty = (float)($ci['quantity'] ?? 0);
if ($qty <= 0) continue;
if (!isset($pr_by_id[$item_id])) {
exit(json_encode(['success' => 0, 'message' => "Item #{$item_id} not found in this purchase request."]));
}
$pi = $pr_by_id[$item_id];
$remaining = (float)$pi['remaining_qty'];
if ($qty - $remaining > 0.000001) {
$name = $pi['product_name'] ?: $pi['product_sku'];
exit(json_encode([
'success' => 0,
'message' => "Cannot convert {$qty} for \"{$name}\": only {$remaining} remaining.",
]));
}
$unit_price = (float)$pi['unit_price'];
$total_price = round($qty * $unit_price, 4);
$tax_amount = round($total_price * (float)($pi['tax_amount'] / max((float)$pi['total_price'], 0.000001)), 4);
$po_items[] = [
'item_id' => $item_id - 1,
'product_sku' => $pi['product_sku'],
'product_name' => $pi['product_name'],
'quantity' => $qty,
'unit_price' => $unit_price,
'total_price' => $total_price,
'tax_amount' => $tax_amount,
'received_qty' => 0,
'stock_in_id' => 0,
];
$validated[] = ['item_id' => $item_id, 'quantity' => $qty];
}
if (empty($po_items)) {
exit(json_encode(['success' => 0, 'message' => 'No valid items to convert.']));
}
$po_data = [
'source' => 'purchase_request',
'source_id' => $request_id,
'contact_id' => $contact_id,
'department_id' => (int)($pr['department_id'] ?? 0),
'po_date' => date('Y-m-d'),
'expected_date' => $pr['required_date'] ?: '',
'warehouse_id' => 0,
'items' => $po_items,
'discount' => $discount,
'shipping_fee' => $shipping_fee,
'notes' => $pr['notes'] ?? '',
];
try {
$new_po_id = null;
dbTransaction($pdo2, function($pdo) use (
$po_data, $company_id, $logging, $request_id, $validated, $prm, &$new_po_id
) {
$po = new PurchaseOrderManager($pdo, $company_id);
$new_po_id = $po->savePo($po_data, $logging);
$prm2 = new PurchaseRequestManager($pdo, $company_id);
$prm2->incrementConvertedQty($request_id, $validated);
});
$answer['success'] = 1;
$answer['message'] = 'Purchase order created.';
$answer['po_id'] = $new_po_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,34 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PurchaseRequestManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$action = $data['action'] ?? '';
$id = (int)($data['id'] ?? 0);
$items = json_decode($data['items'] ?? '[]', true) ?: [];
$prm = new PurchaseRequestManager($pdo2, $company_id);
try {
if ($action === 'create') {
$new_id = $prm->save(array_merge($data, ['id' => 0, 'items' => $items]), $logging);
$answer['success'] = 1;
$answer['message'] = 'Purchase request created.';
$answer['new_id'] = $new_id;
} elseif ($action === 'update') {
$prm->save(array_merge($data, ['id' => $id, 'items' => $items]), $logging);
$answer['success'] = 1;
$answer['message'] = 'Purchase request updated.';
} else {
throw new Exception('Unknown action.');
}
} 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,29 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require_role($user_role, ['owner', 'admin', 'staff']);
require '../../../assets/utils/classes/InvoiceManager.php';
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
$inv = new InvoiceManager($pdo, $company_id);
$new_id = $inv->createManualDebitNote($data, $logging);
});
$answer['success'] = 1;
$answer['new_id'] = $new_id;
$answer['message'] = 'Supplier credit note created.';
} 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,21 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PurchaseRequestManager.php';
$id = (int)($data['id'] ?? 0);
$prm = new PurchaseRequestManager($pdo2, $company_id);
if ($id > 0) {
$row = $prm->getById($id);
if (!$row) {
http_response_code(404);
exit(json_encode(['success' => 0, 'message' => 'Purchase request not found.']));
}
$answer['output'] = $row;
} else {
$answer['output'] = $prm->getList();
}
$answer['success'] = 1;
exit(json_encode($answer));
@@ -0,0 +1,28 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PurchaseRequestManager.php';
require_role($user_role, ['owner', 'admin', 'staff']);
$id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
if (!$id || !$action) {
exit(json_encode(['success' => 0, 'message' => 'ID and action are required.']));
}
$prm = new PurchaseRequestManager($pdo2, $company_id);
try {
$prm->updateStatus($id, $action, $logging);
$answer['success'] = 1;
$answer['message'] = 'Status updated.';
} 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));
+356
View File
@@ -0,0 +1,356 @@
<?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_ac.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?>expense/purchase_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">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>
<a href="#" class="btn btn-outline-info w-100 d-none" id="btn_create_scn">
<i class="ti ti-file-minus me-1"></i>Create Supplier Credit Note
</a>
<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);
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');
$('#btn_create_scn')
.toggleClass('d-none', !(is_pi && status >= 1 && status !== 4))
.attr('href', '<?php echo $server_url?>expense/manage_supplier_credit_note.php?ref_invoice_id=' + invoice_id);
set_btn_state('#btn_void', is_pi && status >= 1 && status !== 4,
!is_pi ? 'Only purchase invoices can be voided'
: status === 0 ? 'Invoice has not been issued yet'
: 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_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?>expense/manage_purchase_order.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?>expense/purchase_invoice.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?>expense/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 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() {
if (invoice_id) retrieve_invoice();
flatpickr('#due_date', { dateFormat: 'd/m/Y', allowInput: true });
});
</script>
</body>
</html>
+364
View File
@@ -0,0 +1,364 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$po_id = (int)($_GET['id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.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 $po_id ? 'Purchase Order Detail' : 'New Purchase Order'; ?></h1>
<p class="mb-0 text-muted" id="page_subtitle">Create a supplier PO for WMS receiving</p>
</div>
<a href="<?php echo $server_url?>expense/purchase_order.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-lg-8">
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Order Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Supplier</label>
<input type="text" id="contact" class="form-control contact_search" placeholder="Search supplier..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">PO Date</label>
<input type="text" id="po_date" class="form-control" placeholder="DD/MM/YYYY">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Expected Delivery <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" id="expected_date" class="form-control" placeholder="DD/MM/YYYY">
</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"></textarea>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Items to Order</h2>
<button class="btn btn-sm btn-outline-primary" id="btn_add_item" onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Item
</button>
</div>
<div class="table-responsive">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Product</th>
<th style="width:90px;">Qty</th>
<th style="width:120px;">Unit Price</th>
<th style="width:80px;">Tax%</th>
<th style="width:120px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<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>
<div class="mb-2" id="badge_status"></div>
<p class="mb-0 small fw-semibold text-muted" id="po_number_display"></p>
</div>
</div>
<div class="alert alert-warning mb-5" id="wms_status_alert">
<div class="d-flex gap-2">
<i class="ti ti-building-warehouse mt-1"></i>
<div>
<div class="fw-semibold" id="wms_status_title">Pending Warehouse receiving setup</div>
<div class="small" id="wms_status_message">WMS staff assign the receiving warehouse, confirm the PO, and receive goods.</div>
</div>
</div>
</div>
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">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">Discount</span><input type="number" id="discount" class="form-control form-control-sm text-end" style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()"></div>
<div class="d-flex justify-content-between mb-2"><span class="text-muted">Tax</span><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><input type="number" id="shipping_fee" class="form-control form-control-sm text-end" style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()"></div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5"><span>Grand Total</span><span id="summary_grand_total">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_po()">
<i class="ti ti-device-floppy me-1"></i>Save Pending PO
</button>
<a href="#" class="btn btn-outline-primary w-100 d-none" id="btn_view_pi">
<i class="ti ti-file-invoice me-1"></i>View Purchase Invoice
</a>
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="cancel_po()">
<i class="ti ti-x me-1"></i>Cancel PO
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var po_id = <?php echo $po_id; ?>;
var po_data = null;
var item_index = 0;
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>',
};
return map[String(status)] || '—';
}
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 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_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
var shipping = parseFloat($('#shipping_fee').val()) || 0;
var derived_tax = round_dp(tax_sum, 2);
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_tax').text(format_number(derived_tax, 2));
$('#summary_grand_total').text(format_number(subtotal - discount + derived_tax + tax_adjustment + shipping, 2));
}
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_sku product_search" placeholder="SKU..." value="${escape_html(item.product_sku || '')}" autocomplete="off" data-product-name-target=".item_name">
<input type="hidden" class="item_name" value="${escape_html(item.product_name || '')}">
<input type="hidden" class="item_id" value="${item.item_id ?? idx}">
<small class="text-muted item_name_display">${escape_html(item.product_name || '')}</small>
</td>
<td><input type="number" class="form-control form-control-sm item_qty" value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()"></td>
<td><input type="number" class="form-control form-control-sm item_price" value="${item.unit_price || item.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 || item.price || 0), 2)}</td>
<td><a href="javascript:void(0);" class="link-danger remove_item_btn" onclick="remove_item(${idx})"><i class="ti ti-trash fs-5"></i></a></td>
</tr>`;
$('#item_tbody').append(row);
if (window.init_product_search_inputs) window.init_product_search_inputs(`#item_row_${idx}`);
recalc_totals();
}
function collect_items() {
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 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,
product_sku: sku,
product_name: name,
quantity: qty,
unit_price: price,
total_price: round_dp(qty * price, 4),
tax_rate: tax_rate,
tax_amount: tax_amt,
received_qty: 0,
stock_in_id: 0,
});
idx++;
});
return items;
}
function set_mode(status, res) {
status = parseInt(status);
res = res || {};
var editable = status === -2;
$('#contact, #po_date, #expected_date, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee, #btn_add_item')
.prop('disabled', !editable);
$('.item_sku, .item_qty, .item_price, .item_tax_rate').prop('disabled', !editable);
$('.remove_item_btn').toggleClass('d-none', !editable);
$('#btn_save').toggleClass('d-none', !editable);
$('#btn_cancel').toggleClass('d-none', !(status === -2 || status === 0));
if (status === -2) {
$('#wms_status_title').text('Pending Warehouse receiving setup');
$('#wms_status_message').text('WMS staff assign the receiving warehouse, confirm the PO, and receive goods.');
} else {
$('#wms_status_title').text('Managed on WMS side');
$('#wms_status_message').text('The PO has moved beyond accounting edits. Continue warehouse updates from the WMS PO screen.');
}
$('#btn_view_pi')
.toggleClass('d-none', !res.has_active_pi)
.attr('href', res.active_pi_id ? '<?php echo $server_url?>expense/manage_purchase_invoice.php?id=' + res.active_pi_id : '#');
}
function retrieve_po() {
if (!po_id) 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: po_id },
onSuccess: function(res) {
po_data = res.output || {};
$('#page_title').text(po_data.po_number || 'Purchase Order Detail');
$('#status_card').removeClass('d-none');
$('#badge_status').html(po_status_badge(po_data.status));
$('#po_number_display').text(po_data.po_number || '');
$('#contact').val(po_data.contact_name || '');
$('#contact_id').val(po_data.contact_id || '');
$('#po_date').val(po_data.po_date ? format_date(po_data.po_date) : '');
$('#expected_date').val(po_data.expected_date ? format_date(po_data.expected_date) : '');
$('#department_id').val(po_data.department_id || 0);
$('#notes').val(po_data.notes || '');
$('#discount').val(po_data.discount || 0);
$('#tax_adjustment').val(po_data.tax_adjustment || 0);
$('#shipping_fee').val(po_data.shipping_fee || 0);
$('#item_tbody').empty();
item_index = 0;
(po_data.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(po_data.status, res);
}
});
}
function save_po() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one item.');
return;
}
ajax_request({
url: '<?php echo $server_url?>po/api/engine/manage_po.php',
autoPrepare: true,
checkRequired: 0,
action: po_id ? 'update' : 'create',
data: {
id: po_id,
source: po_id ? '' : 'purchase',
source_id: 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: po_data ? (po_data.warehouse_id || 0) : 0,
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax_adjustment: $('#tax_adjustment').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!po_id && res.new_id) {
window.location.href = '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + res.new_id;
} else {
retrieve_po();
}
}
});
}
function cancel_po() {
bootbox.confirm('Cancel this purchase order?', function(ok) {
if (!ok) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/cancel_po.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: po_id },
onSuccess: function() { retrieve_po(); }
});
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
$(document).on('product:selected', '.item_sku', function(e, product) {
var $row = $(this).closest('tr');
$row.find('.item_name').val(product.product_name || '');
$row.find('.item_name_display').text(product.product_name || '');
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
recalc_totals();
});
$(function() {
load_departments('department_id');
flatpickr('#po_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#expected_date', { dateFormat: 'd/m/Y', allowInput: true });
if (po_id) {
retrieve_po();
} else {
$('#po_date').val(flatpickr.formatDate(new Date(), 'd/m/Y'));
add_item_row();
set_mode(-2, {});
}
});
</script>
</body>
</html>
+365
View File
@@ -0,0 +1,365 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$request_id = (int)($_GET['id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.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 $request_id ? 'Purchase Request Detail' : 'New Purchase Request'; ?></h1>
<p class="mb-0 text-muted" id="page_subtitle"><?php echo $request_id ? '' : 'Create an internal request before purchase order handoff'; ?></p>
</div>
<a href="<?php echo $server_url?>expense/purchase_request.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-lg-8">
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Request Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Preferred Supplier</label>
<input type="text" id="contact" class="form-control contact_search" placeholder="Search supplier..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">Request Date</label>
<input type="text" id="request_date" class="form-control" placeholder="DD/MM/YYYY">
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">Required Date</label>
<input type="text" id="required_date" class="form-control" placeholder="DD/MM/YYYY">
</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-12">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Requested Items</h2>
<button class="btn btn-sm btn-outline-primary" id="btn_add_item" onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Item
</button>
</div>
<div class="table-responsive">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th style="width:130px;">SKU</th>
<th>Description</th>
<th style="width:90px;">Qty</th>
<th style="width:120px;">Unit Price</th>
<th style="width:80px;">Tax%</th>
<th style="width:120px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<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>
<div id="badge_status" class="mb-2"></div>
<p class="mb-0 small fw-semibold text-muted" id="request_number_display"></p>
<p class="mb-0 small text-muted d-none" id="po_display"></p>
</div>
</div>
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">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 align-items-center mb-2"><span class="text-muted">Discount</span><input type="number" id="discount" class="form-control form-control-sm text-end" style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()"></div>
<div class="d-flex justify-content-between mb-2"><span class="text-muted">Tax</span><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 align-items-center mb-3"><span class="text-muted">Shipping</span><input type="number" id="shipping_fee" class="form-control form-control-sm text-end" style="width:110px;" value="0" min="0" step="any" oninput="recalc_totals()"></div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5"><span>Grand Total</span><span id="summary_grand_total">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_request()"><i class="ti ti-device-floppy me-1"></i>Save Draft</button>
<button class="btn btn-info text-white w-100 d-none" id="btn_submit" onclick="change_status('submit')"><i class="ti ti-send me-1"></i>Submit Request</button>
<button class="btn btn-success w-100 d-none" id="btn_approve" onclick="change_status('approve')"><i class="ti ti-circle-check me-1"></i>Approve</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_reject" onclick="change_status('reject')"><i class="ti ti-x me-1"></i>Reject</button>
<button class="btn btn-outline-secondary w-100 d-none" id="btn_reopen" onclick="change_status('reopen')"><i class="ti ti-refresh me-1"></i>Reopen as Draft</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="change_status('cancel')"><i class="ti ti-ban me-1"></i>Cancel</button>
<hr class="d-none" id="hr_convert">
<button class="btn btn-success w-100 d-none" id="btn_convert" onclick="convert_to_po()"><i class="ti ti-transfer me-1"></i>Convert to PO</button>
<a href="#" class="btn btn-outline-primary w-100 d-none" id="btn_view_po"><i class="ti ti-clipboard-list me-1"></i>View PO</a>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var request_id = <?php echo $request_id; ?>;
var request_data = null;
var item_index = 0;
var status_badge = {
'-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">Submitted</span>',
'2': '<span class="badge bg-success">Approved</span>',
'3': '<span class="badge bg-danger">Rejected</span>',
'5': '<span class="badge bg-primary">Converted</span>',
};
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 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_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
var shipping = parseFloat($('#shipping_fee').val()) || 0;
var derived_tax = round_dp(tax_sum, 2);
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_tax').text(format_number(derived_tax, 2));
$('#summary_grand_total').text(format_number(subtotal - discount + derived_tax + tax_adjustment + shipping, 2));
}
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td><input type="text" class="form-control form-control-sm item_sku product_search" value="${escape_html(item.product_sku || '')}" placeholder="SKU..." autocomplete="off" data-product-name-target=".item_desc"></td>
<td><input type="text" class="form-control form-control-sm item_desc" value="${escape_html(item.product_name || item.description || '')}" placeholder="Description..."></td>
<td><input type="number" class="form-control form-control-sm item_qty" value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()"></td>
<td><input type="number" class="form-control form-control-sm item_price" value="${item.unit_price || item.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 align-middle">${format_number((item.quantity || 1) * (item.unit_price || item.price || 0), 2)}</td>
<td class="align-middle"><a href="javascript:void(0);" class="link-danger remove_item_btn" onclick="remove_item(${idx})"><i class="ti ti-trash fs-5"></i></a></td>
</tr>`;
$('#item_tbody').append(row);
if (window.init_product_search_inputs) window.init_product_search_inputs(`#item_row_${idx}`);
recalc_totals();
}
function collect_items() {
var items = [];
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var desc = $(this).find('.item_desc').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 && !desc && qty <= 0) return;
items.push({
product_sku: sku,
product_name: desc,
description: desc,
quantity: qty,
unit_price: price,
total_price: round_dp(qty * price, 4),
tax_rate: tax_rate,
tax_amount: tax_amt,
});
});
return items;
}
function set_mode(status) {
status = parseInt(status);
var is_draft = status === 0;
var is_submitted = status === 1;
var is_approved = status === 2;
var is_closed = status === -1 || status === 3 || status === 5;
$('#contact, #request_date, #required_date, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee, #btn_add_item').prop('disabled', !is_draft);
$('.item_sku, .item_desc, .item_qty, .item_price, .item_tax_rate').prop('disabled', !is_draft);
$('.remove_item_btn').toggleClass('d-none', !is_draft);
$('#btn_save').toggleClass('d-none', !is_draft);
$('#btn_submit').toggleClass('d-none', !is_draft || !request_id);
$('#btn_approve, #btn_reject').toggleClass('d-none', !is_submitted);
$('#btn_reopen').toggleClass('d-none', is_draft || is_closed);
$('#btn_cancel').toggleClass('d-none', !(is_draft || is_submitted) || !request_id);
$('#hr_convert, #btn_convert').toggleClass('d-none', !is_approved);
$('#btn_view_po').toggleClass('d-none', !(request_data && parseInt(request_data.po_id) > 0))
.attr('href', request_data && request_data.po_id ? '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + request_data.po_id : '#');
}
function retrieve_request() {
if (!request_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>expense/api/engine/retrieve_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: request_id },
onSuccess: function(res) {
request_data = res.output || {};
$('#page_title').text(request_data.request_number || 'Purchase Request Detail');
$('#status_card').removeClass('d-none');
$('#badge_status').html(status_badge[String(request_data.status)] || request_data.status);
$('#request_number_display').text(request_data.request_number || '');
$('#po_display')
.toggleClass('d-none', !(request_data.po_id > 0))
.html(request_data.po_id > 0 ? 'PO: <a href="<?php echo $server_url?>expense/manage_purchase_order.php?id=' + request_data.po_id + '">' + escape_html(request_data.po_number || ('PO #' + request_data.po_id)) + '</a>' : '');
$('#contact').val(request_data.contact_name || '');
$('#contact_id').val(request_data.contact_id || 0);
$('#request_date').val(request_data.request_date ? format_date(request_data.request_date) : '');
$('#required_date').val(request_data.required_date ? format_date(request_data.required_date) : '');
$('#department_id').val(request_data.department_id || 0);
$('#notes').val(request_data.notes || '');
$('#discount').val(request_data.discount || 0);
$('#tax_adjustment').val(request_data.tax_adjustment || 0);
$('#shipping_fee').val(request_data.shipping_fee || 0);
$('#item_tbody').empty();
item_index = 0;
(request_data.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(request_data.status);
}
});
}
function save_request() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one item.');
return;
}
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/manage_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: request_id ? 'update' : 'create',
data: {
id: request_id,
contact_id: $('#contact_id').val() || 0,
department_id: $('#department_id').val() || 0,
request_date: to_iso_date($('#request_date').val()),
required_date: to_iso_date($('#required_date').val()),
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax_adjustment: $('#tax_adjustment').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!request_id && res.new_id) window.location.href = '<?php echo $server_url?>expense/manage_purchase_request.php?id=' + res.new_id;
else retrieve_request();
}
});
}
function change_status(action_type) {
var labels = {
submit: 'Submit this purchase request?',
approve: 'Approve this purchase request?',
reject: 'Reject this purchase request?',
reopen: 'Reopen this purchase request as Draft?',
cancel: 'Cancel this purchase request?',
};
bootbox.confirm(labels[action_type] || 'Confirm?', function(ok) {
if (!ok) return;
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/update_purchase_request_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: request_id, action_type: action_type },
onSuccess: function() { retrieve_request(); }
});
});
}
function convert_to_po() {
bootbox.confirm('Convert this approved purchase request to a pending warehouse purchase order?', function(ok) {
if (!ok) return;
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/convert_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: 'create',
data: { request_id: request_id },
onSuccess: function(res) {
window.location.href = '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + res.po_id;
}
});
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
$(document).on('product:selected', '.item_sku', function(e, product) {
var $row = $(this).closest('tr');
var price = parseFloat(product.price || 0);
$row.find('.item_desc').val(product.product_name || '');
if (!(parseFloat($row.find('.item_price').val()) > 0) && price > 0) {
$row.find('.item_price').val(price);
}
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
recalc_totals();
});
$(function() {
load_departments('department_id');
flatpickr('#request_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#required_date', { dateFormat: 'd/m/Y', allowInput: true });
if (request_id) {
retrieve_request();
} else {
$('#request_date').val(flatpickr.formatDate(new Date(), 'd/m/Y'));
add_item_row();
set_mode(0);
}
});
</script>
</body>
</html>
+269
View File
@@ -0,0 +1,269 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$ref_invoice_id = (int)($_GET['ref_invoice_id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.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">New Supplier Credit Note</h1>
<p class="mb-0 text-muted">Accounting supplier credit note without stock movement</p>
</div>
<a href="<?php echo $server_url?>expense/purchase_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">
<div class="col-lg-8">
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Supplier Credit Note Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Reference Purchase Invoice ID</label>
<input type="number" id="ref_invoice_id" class="form-control"
value="<?php echo $ref_invoice_id ?: ''; ?>" min="0" placeholder="Optional">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Supplier</label>
<input type="text" id="contact" class="form-control contact_search"
placeholder="Search supplier..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Reason / Notes</label>
<textarea id="notes" class="form-control" rows="3"
placeholder="Billing correction, returns credit, price adjustment..."></textarea>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Credit Lines</h2>
<button class="btn btn-sm btn-outline-primary" onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Line
</button>
</div>
<div class="table-responsive">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Description</th>
<th style="width:100px;">Qty</th>
<th style="width:130px;">Unit Price</th>
<th style="width:80px;">Tax%</th>
<th style="width:120px;" class="text-end">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">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>Credit Total</span>
<span id="summary_grand_total" class="text-info">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" onclick="save_supplier_credit_note()">
<i class="ti ti-device-floppy me-1"></i>Save Draft
</button>
<a href="<?php echo $server_url?>expense/purchase_invoice.php" class="btn btn-light w-100">Cancel</a>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var item_index = 0;
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_desc"
value="${escape_html(item.product_name || '')}" placeholder="Credit description">
</td>
<td>
<input type="number" class="form-control form-control-sm item_qty"
value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_price"
value="${item.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()">
</td>
<td class="item_total text-end align-middle">0.00</td>
<td class="align-middle">
<a href="javascript:void(0);" class="link-danger" onclick="remove_item(${idx})">
<i class="ti ti-trash fs-5"></i>
</a>
</td>
</tr>`;
$('#item_tbody').append(row);
recalc_totals();
}
function remove_item(idx) {
$('#item_row_' + idx).remove();
recalc_totals();
}
function recalc_totals() {
var subtotal = 0, 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 tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
var total = qty * price;
subtotal += total;
tax_sum += total * tax_rate / 100;
$(this).find('.item_total').text(format_number(total, 2));
});
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_grand_total').text(format_number(subtotal + tax, 2));
}
function collect_items() {
var items = [];
$('#item_tbody tr').each(function() {
var desc = $(this).find('.item_desc').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;
if (!desc && qty <= 0) return;
items.push({
product_sku: '',
product_name: desc,
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 load_ref_invoice() {
var ref_id = parseInt($('#ref_invoice_id').val()) || 0;
if (!ref_id) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { invoice_id: ref_id },
onSuccess: function(res) {
var inv = res.output || {};
if (inv.doc_type !== 'purchase_invoice') return;
$('#contact').val(inv.contact_name || '');
$('#contact_id').val(inv.contact_id || 0);
}
});
}
function save_supplier_credit_note() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one credit line.');
return;
}
if (!$('#contact_id').val()) {
bootbox.alert('Please select a supplier.');
return;
}
if (!$('#notes').val().trim()) {
bootbox.alert('Please enter the reason for this supplier credit note.');
return;
}
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/manage_supplier_credit_note.php',
autoPrepare: true,
checkRequired: 0,
action: 'create',
data: {
ref_invoice_id: $('#ref_invoice_id').val() || 0,
contact_id: $('#contact_id').val() || 0,
items: JSON.stringify(items),
notes: $('#notes').val(),
tax_adjustment: parseFloat($('#tax_adjustment').val()) || 0,
},
onSuccess: function(res) {
if (res.new_id) {
window.location.href = '<?php echo $server_url?>expense/manage_purchase_invoice.php?id=' + res.new_id;
}
}
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
$(function() {
add_item_row();
load_ref_invoice();
$('#ref_invoice_id').on('change', load_ref_invoice);
});
</script>
</body>
</html>
+2 -34
View File
@@ -1,36 +1,4 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
header('Location: ../finance/payment.php');
exit;
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Payments</h1>
<p class="mb-0 text-muted">Payments to suppliers</p>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-credit-card fs-1 mb-3 d-block"></i>
<p class="mb-0">Payments coming soon.</p>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
</body>
</html>
+385 -5
View File
@@ -13,16 +13,174 @@
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Purchase Invoices</h1>
<p class="mb-0 text-muted">Supplier invoices</p>
<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>
<a href="<?php echo $server_url?>expense/manage_supplier_credit_note.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>New Supplier Credit Note
</a>
</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">Invoices</p>
<h3 class="fw-bold mb-0" id="stat_invoice">—</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">Paid</p>
<h3 class="fw-bold mb-0" id="stat_paid">—</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-clock fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Open</p>
<h3 class="fw-bold mb-0" id="stat_open">—</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-file-minus fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Supplier Credit Notes</p>
<h3 class="fw-bold mb-0" id="stat_scn">—</h3>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-12">
<ul class="nav nav-tabs" id="invoice_tabs">
<li class="nav-item">
<a class="nav-link active" href="#" onclick="switch_tab('purchase_invoice'); return false;">
<i class="ti ti-file-invoice me-1"></i>Purchase Invoices
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="switch_tab('supplier_credit_note'); return false;">
<i class="ti ti-file-minus me-1"></i>Supplier Credit Notes
</a>
</li>
</ul>
</div>
</div>
<div class="row g-5 mb-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_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>
<div class="row">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-file-invoice fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Invoices coming soon.</p>
<div class="card">
<div class="card-body p-5">
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="pi_list">
<thead class="table-primary border-light">
<tr>
<th>Invoice #</th>
<th>Source</th>
<th>Date</th>
<th>Supplier</th>
<th>Department</th>
<th>Grand Total</th>
<th>Due Date</th>
<th>GL Formula</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
<table class="table table-hover mb-0 table-centered d-none" id="scn_list">
<thead class="table-info border-light">
<tr>
<th>SCN #</th>
<th>Ref Invoice #</th>
<th>Supplier</th>
<th>Department</th>
<th>Date</th>
<th>Amount</th>
<th>GL Formula</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -32,5 +190,227 @@
<?php require '../include_ending.php'; ?>
<script>
var current_tab = <?php echo json_encode($_GET['tab'] ?? 'purchase_invoice'); ?>;
var all_invoices = [];
var formula_map = {};
function load_formula_map() {
return ajax_request({
url: '<?php echo $server_url?>accounting/api/engine/account_formula.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: {},
onSuccess: function(res) {
formula_map = {};
$.each(res.output || [], function(i, f) { formula_map[f.id] = f.formula_name; });
}
});
}
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 switch_tab(tab) {
current_tab = tab;
$('#invoice_tabs .nav-link').each(function() {
$(this).toggleClass('active', $(this).attr('onclick').includes("'" + tab + "'"));
});
$('#pi_list').toggleClass('d-none', tab !== 'purchase_invoice');
$('#scn_list').toggleClass('d-none', tab !== 'supplier_credit_note');
if (tab === 'supplier_credit_note') {
$('#filter_status').html(`
<option value="">All Status</option>
<option value="0">Draft</option>
<option value="1">Issued</option>
<option value="4">Void</option>
`);
} else {
$('#filter_status').html(`
<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>
`);
}
retrieve_invoices();
}
function update_stats() {
var pis = all_invoices.filter(i => i.doc_type === 'purchase_invoice');
$('#stat_invoice').text(format_number(pis.length));
$('#stat_paid').text(format_number(pis.filter(i => String(i.status) === '2').length));
$('#stat_open').text(format_number(pis.filter(i => ['0','1'].includes(String(i.status))).length));
$('#stat_scn').text(format_number(all_invoices.filter(i => i.doc_type === 'supplier_credit_note').length));
}
function retrieve_stats_source() {
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { doc_type: '' },
onSuccess: function(res) {
all_invoices = (res.output || []).filter(function(inv) {
return inv.doc_type === 'purchase_invoice' || inv.doc_type === 'supplier_credit_note';
});
update_stats();
}
});
}
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: current_tab },
onSuccess: function(res) {
var all = res.output || [];
var keyword = $('#search_input').val().toLowerCase();
var status = $('#filter_status').val();
var fil_dept = $('#filter_dept').val();
var data = all.filter(function(inv) {
var match_kw = !keyword ||
(inv.invoice_number || '').toLowerCase().includes(keyword) ||
(inv.contact_name || '').toLowerCase().includes(keyword);
var match_st = !status || String(inv.status) === status;
var match_dept = !fil_dept || String(inv.department_id) === fil_dept;
return match_kw && match_st && match_dept;
});
if (current_tab === 'purchase_invoice') {
render_pi_table(data);
} else {
render_scn_table(data);
}
}
});
}
// ── Purchase Invoice table ────────────────────────────────────────────────
function render_pi_table(data) {
alasql('DROP TABLE IF EXISTS pi_list');
alasql('CREATE TABLE pi_list');
alasql.tables.pi_list.data = data;
$('table#pi_list tfoot').html(generate_pagination('pi_list', data.length));
change_page_pi_list(1);
}
function change_page_pi_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var rows = alasql(`SELECT * FROM pi_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
if (!rows.length) {
$('table#pi_list tbody').html('<tr><td colspan="10" class="text-center py-5 text-muted">No purchase invoices found.</td></tr>');
return;
}
$.each(rows, function(i, inv) {
var source_label = '<span class="text-muted">—</span>';
if (inv.source === 'po' && inv.source_id) {
source_label = `<a href="<?php echo $server_url?>expense/manage_purchase_order.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>`;
}
var formula_name = inv.formula_id && formula_map[inv.formula_id]
? escape_html(formula_map[inv.formula_id])
: '<span class="text-muted">—</span>';
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(inv.invoice_number || '')}</td>
<td class="py-3">${source_label}</td>
<td class="py-3">${inv.issued_date ? format_date(inv.issued_date) : '<span class="text-muted">—</span>'}</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 small">${formula_name}</td>
<td class="py-3">${invoice_status_badge(inv.status, inv.due_date)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>expense/manage_purchase_invoice.php?id=${inv.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#pi_list tbody').html(body);
}
// ── Supplier Credit Note table ────────────────────────────────────────────
function render_scn_table(data) {
alasql('DROP TABLE IF EXISTS scn_list');
alasql('CREATE TABLE scn_list');
alasql.tables.scn_list.data = data;
$('table#scn_list tfoot').html(generate_pagination('scn_list', data.length));
change_page_scn_list(1);
}
function change_page_scn_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var rows = alasql(`SELECT * FROM scn_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
if (!rows.length) {
$('table#scn_list tbody').html('<tr><td colspan="9" class="text-center py-5 text-muted">No supplier credit notes found.</td></tr>');
return;
}
$.each(rows, function(i, scn) {
var formula_name = scn.formula_id && formula_map[scn.formula_id]
? escape_html(formula_map[scn.formula_id])
: '<span class="text-muted">—</span>';
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(scn.invoice_number || '')}</td>
<td class="py-3">${scn.ref_invoice_number ? escape_html(scn.ref_invoice_number) : '<span class="text-muted">—</span>'}</td>
<td class="py-3">${escape_html(scn.contact_name || '—')}</td>
<td class="py-3">${get_dept_label(scn.department_id)}</td>
<td class="py-3">${scn.issued_date ? format_date(scn.issued_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3 fw-semibold text-info">${format_number(scn.grand_total, 2)}</td>
<td class="py-3 small">${formula_name}</td>
<td class="py-3">${invoice_status_badge(scn.status, null)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>expense/manage_purchase_invoice.php?id=${scn.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#scn_list tbody').html(body);
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
await retrieve_stats_source();
await load_formula_map();
$('#search_input').on('keyup', function(e) { if (e.key === 'Enter') retrieve_invoices(); });
$('#filter_status, #filter_dept').on('change', retrieve_invoices);
if (current_tab !== 'purchase_invoice') {
switch_tab(current_tab);
} else {
await retrieve_invoices();
}
});
</script>
</body>
</html>
+218 -6
View File
@@ -13,16 +13,128 @@
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Purchase Orders</h1>
<p class="mb-0 text-muted">Purchase order management</p>
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Purchase Orders</h1>
<p class="mb-0 text-muted">Supplier orders handed off to WMS receiving</p>
</div>
<a href="<?php echo $server_url?>expense/manage_purchase_order.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>New Purchase Order
</a>
</div>
</div>
</div>
<div class="row">
<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-clipboard-list fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Total POs</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-danger bg-opacity-10 text-danger rounded-2">
<i class="ti ti-building-warehouse fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Pending Warehouse</p>
<h3 class="fw-bold mb-0" id="stat_pending">—</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">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-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">Received</p>
<h3 class="fw-bold mb-0" id="stat_received">—</h3>
</div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-clipboard-list fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Orders coming soon.</p>
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<div class="col-lg-4">
<input type="text" id="search_input" class="form-control" placeholder="Search PO number or supplier...">
</div>
<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="0">Draft</option>
<option value="1">Confirmed</option>
<option value="-1">Cancelled</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_pos()">
<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="po_list">
<thead class="table-primary border-light">
<tr>
<th>PO #</th>
<th>Source</th>
<th>Date</th>
<th>Supplier</th>
<th>Department</th>
<th>Grand Total</th>
<th>Status</th>
<th>Receiving</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -32,5 +144,105 @@
<?php require '../include_ending.php'; ?>
<script>
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>',
};
return map[String(status)] || '—';
}
function receipt_status_badge(status) {
const map = {
'2': '<span class="badge bg-primary">Partial</span>',
'3': '<span class="badge bg-success">Received</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',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var all = (res.output || []).filter(function(po) {
return ['purchase', 'purchase_request'].includes(String(po.source || ''));
});
var keyword = $('#search_input').val().toLowerCase();
var status = $('#filter_status').val();
var fil_dept = $('#filter_dept').val();
var data = all.filter(function(po) {
var source_id = String(po.source_id || '');
var source = String(po.source || '');
var source_label = source === 'purchase' ? 'direct' : 'purchase request';
var match_kw = !keyword ||
String(po.po_number || '').toLowerCase().includes(keyword) ||
String(po.contact_name || '').toLowerCase().includes(keyword) ||
source_label.includes(keyword) ||
source_id.includes(keyword);
var match_status = status === '' || String(po.status) === status;
var match_dept = !fil_dept || String(po.department_id) === fil_dept;
return match_kw && match_status && match_dept;
});
$('#stat_total').text(format_number(all.length));
$('#stat_pending').text(format_number(all.filter(po => String(po.status) === '-2').length));
$('#stat_confirmed').text(format_number(all.filter(po => String(po.status) === '1').length));
$('#stat_received').text(format_number(all.filter(po => String(po.receipt_status) === '3').length));
alasql('CREATE TABLE IF NOT EXISTS purchase_po_list');
alasql.tables.purchase_po_list.data = data;
$('table#po_list tfoot').html(generate_pagination('po_list', data.length));
change_page_po_list(1);
}
});
}
function change_page_po_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var rows = alasql(`SELECT * FROM purchase_po_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
if (!rows.length) {
$('table#po_list tbody').html('<tr><td colspan="8" class="text-center py-5 text-muted">No purchase orders found.</td></tr>');
return;
}
$.each(rows, function(i, po) {
var source = String(po.source || '');
var source_display = source === 'purchase_request' && parseInt(po.source_id) > 0
? `<a href="<?php echo $server_url?>expense/manage_purchase_request.php?id=${po.source_id}">Request #${po.source_id}</a>`
: (source === 'purchase' ? 'Direct' : '<span class="text-muted">—</span>');
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(po.po_number || '')}</td>
<td class="py-3">${source_display}</td>
<td class="py-3">${po.po_date ? format_date(po.po_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3">${escape_html(po.contact_name || '—')}</td>
<td class="py-3">${get_dept_label(po.department_id)}</td>
<td class="py-3 fw-semibold">${format_number(po.grand_total, 2)}</td>
<td class="py-3">${po_status_badge(po.status)}</td>
<td class="py-3">${receipt_status_badge(po.receipt_status)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>expense/manage_purchase_order.php?id=${po.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#po_list tbody').html(body);
}
$(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>
+184 -6
View File
@@ -13,16 +13,111 @@
<div class="row mb-6">
<div class="col-12">
<h1 class="fs-3 mb-1">Purchase Requests</h1>
<p class="mb-0 text-muted">Internal purchase requests</p>
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Purchase Requests</h1>
<p class="mb-0 text-muted">Internal purchase requests before purchase order handoff</p>
</div>
<a href="<?php echo $server_url?>expense/manage_purchase_request.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>New Purchase Request
</a>
</div>
</div>
</div>
<div class="row">
<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-notes 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">Submitted</p><h3 class="fw-bold mb-0" id="stat_submitted">—</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">Approved</p><h3 class="fw-bold mb-0" id="stat_approved">—</h3></div>
</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-12">
<div class="card p-5 text-center text-muted">
<i class="ti ti-notes fs-1 mb-3 d-block"></i>
<p class="mb-0">Purchase Requests coming soon.</p>
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<div class="col-lg-4">
<input type="text" id="search_input" class="form-control" placeholder="Search request number or supplier...">
</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">Submitted</option>
<option value="2">Approved</option>
<option value="3">Rejected</option>
<option value="5">Converted</option>
<option value="-1">Cancelled</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_requests()">
<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="request_list">
<thead class="table-primary border-light">
<tr>
<th>Request #</th>
<th>Date</th>
<th>Required</th>
<th>Supplier</th>
<th>Department</th>
<th>Items</th>
<th>Grand Total</th>
<th>Status</th>
<th>PO</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
@@ -32,5 +127,88 @@
<?php require '../include_ending.php'; ?>
<script>
var status_badge = {
'-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">Submitted</span>',
'2': '<span class="badge bg-success">Approved</span>',
'3': '<span class="badge bg-danger">Rejected</span>',
'5': '<span class="badge bg-primary">Converted</span>',
};
function retrieve_requests() {
return ajax_request({
url: '<?php echo $server_url?>expense/api/engine/retrieve_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var all = res.output || [];
var keyword = $('#search_input').val().toLowerCase();
var status = $('#filter_status').val();
var fil_dept = $('#filter_dept').val();
var data = all.filter(function(r) {
var match_kw = !keyword ||
String(r.request_number || '').toLowerCase().includes(keyword) ||
String(r.contact_name || '').toLowerCase().includes(keyword);
var match_st = status === '' || String(r.status) === 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_submitted').text(format_number(all.filter(r => String(r.status) === '1').length));
$('#stat_approved').text(format_number(all.filter(r => String(r.status) === '2').length));
alasql('CREATE TABLE IF NOT EXISTS purchase_request_list');
alasql.tables.purchase_request_list.data = data;
$('table#request_list tfoot').html(generate_pagination('request_list', data.length));
change_page_request_list(1);
}
});
}
function change_page_request_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var rows = alasql(`SELECT * FROM purchase_request_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
if (!rows.length) {
$('table#request_list tbody').html('<tr><td colspan="9" class="text-center py-5 text-muted">No purchase requests found.</td></tr>');
return;
}
$.each(rows, function(i, r) {
var items = typeof r.items === 'string' ? JSON.parse(r.items || '[]') : (r.items || []);
var po_link = r.po_id > 0
? `<a href="<?php echo $server_url?>expense/manage_purchase_order.php?id=${r.po_id}">${escape_html(r.po_number || ('PO #' + r.po_id))}</a>`
: '<span class="text-muted">—</span>';
body += `<tr>
<td class="py-3 fw-semibold">${escape_html(r.request_number || '')}</td>
<td class="py-3">${r.request_date ? format_date(r.request_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3">${r.required_date ? format_date(r.required_date) : '<span class="text-muted">—</span>'}</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">${items.length} item${items.length !== 1 ? 's' : ''}</td>
<td class="py-3 fw-semibold">${format_number(r.grand_total, 2)}</td>
<td class="py-3">${status_badge[String(r.status)] || r.status}</td>
<td class="py-3">${po_link}</td>
<td class="py-3">
<a href="<?php echo $server_url?>expense/manage_purchase_request.php?id=${r.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#request_list tbody').html(body);
}
$(async function() {
await load_dept_cache();
populate_dept_filter('filter_dept');
retrieve_requests();
$('#search_input').on('keyup', function(e) { if (e.key === 'Enter') retrieve_requests(); });
$('#filter_status, #filter_dept').on('change', retrieve_requests);
});
</script>
</body>
</html>