365 lines
16 KiB
PHP
365 lines
16 KiB
PHP
<?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>
|