Fix transfer quantity, unify date format, align demo seeds

Stock Transfer list showed 0.00 (read in instead of out); stock-out/transfer forms show the location quantity; dates display as YYYY-MM-DD HH:mm:ss. Demo seeds map product accounts and use product names and supplier batches.
This commit is contained in:
Thanakorn
2026-09-15 13:09:19 +07:00
parent 9512d440dd
commit 693c72f9ea
25 changed files with 231 additions and 212 deletions
+1 -1
View File
@@ -509,7 +509,7 @@
var extra_fields = h.source_type === 'manual'
? '<div class="col-sm-4"><div class="text-muted small">Reference</div><div class="fw-semibold">' + escape_html(h.reference || ('MJE-' + h.id)) + '</div></div>' +
'<div class="col-sm-4"><div class="text-muted small">Journal Date</div><div>' + escape_html(h.journal_date_fmt || '—') + '</div></div>' +
'<div class="col-sm-4"><div class="text-muted small">Journal Date</div><div>' + escape_html(format_date(h.journal_date)) + '</div></div>' +
'<div class="col-sm-4"><div class="text-muted small">Description</div><div>' + escape_html(h.description || '—') + '</div></div>'
: '<div class="col-sm-4"><div class="text-muted small">Formula</div><div>' + escape_html(h.formula_name) + '</div></div>' +
'<div class="col-sm-4"><div class="text-muted small">Version</div><div>v' + h.current_version + (h.current_version > 1 ? ' <span class="text-muted small">(replaced)</span>' : '') + '</div></div>' +
+11 -2
View File
@@ -995,15 +995,24 @@ document.addEventListener('DOMContentLoaded', () => {
/**
* Helper function to format date strings (assuming input is in ISO format)
*/
// Display format used across the app: YYYY-MM-DD, or YYYY-MM-DD HH:mm:ss when the value has a time.
function format_date(iso_string) {
if (!iso_string) return '—';
var split = iso_string.split(" ");
var split = String(iso_string).split(" ");
var datePart = split[0].split("-");
if (datePart.length !== 3) return iso_string;
var formatted = `${datePart[2]}/${datePart[1]}/${datePart[0]}`;
var formatted = `${datePart[0]}-${datePart[1]}-${datePart[2]}`;
return split.length === 2 ? `${formatted} ${split[1]}` : formatted;
}
// DD/MM/YYYY for filling date inputs (flatpickr dateFormat 'd/m/Y'); to_iso_date() reverses it.
function format_date_input(iso_string) {
if (!iso_string) return '';
var datePart = String(iso_string).split(" ")[0].split("-");
if (datePart.length !== 3) return iso_string;
return `${datePart[2]}/${datePart[1]}/${datePart[0]}`;
}
function to_iso_date(dateStr) {
if (!dateStr) return null;
+8 -5
View File
@@ -79,7 +79,8 @@ class StockManager {
public function getStockList(int $warehouse_id, string $type): array
{
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
$column = $type === 'out' ? 'ROUND(a.out, 2)' : 'ROUND(a.in, 2)';
// The transfer list shows the outbound row, whose quantity is in `out` (its `in` is always 0).
$column = in_array($type, ['out', 'transfer'], true) ? 'ROUND(a.out, 2)' : 'ROUND(a.in, 2)';
$stock_ref = $this->stockReferenceSql();
// Transfer list: show only the outbound side (out > 0) to avoid duplicate display
@@ -439,8 +440,9 @@ class StockManager {
);
}
// Quantity and identifiers come from the existing stock_in row (immutable)
$quantity = (int)$source_stock['in'];
// Quantity and identifiers come from the existing stock_in row (immutable).
// `in` is decimal(18,4) — an int cast would drop fractional quantities.
$quantity = (float)$source_stock['in'];
$ref_id = (int)$source_stock['id'];
$lot_number = $source_stock['lot_number'] ?? null;
$serial_number = $source_stock['serial_number'] ?? null;
@@ -611,8 +613,9 @@ class StockManager {
);
}
// Quantity and identifiers come from the source stock_in row (immutable)
$quantity = (int)$source_stock['in'];
// Quantity and identifiers come from the source stock_in row (immutable).
// `in` is decimal(18,4) — an int cast would drop fractional quantities.
$quantity = (float)$source_stock['in'];
$lot_number = $source_stock['lot_number'] ?? null;
$serial_number = $source_stock['serial_number'] ?? null;
@@ -697,7 +697,7 @@ class FinancialReports
COALESCE(d.dept_code, '') AS dept_code,
COALESCE(d.dept_name, '') AS dept_name,
COALESCE(g.journal_date, DATE(g.created_at)) AS entry_date,
DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at,
DATE_FORMAT(g.created_at, '%Y-%m-%d %H:%i:%s') AS posted_at,
i.debit,
i.credit,
COALESCE(i.description, '') AS line_description
@@ -75,8 +75,8 @@ class GlQueryManager
g.current_version,
g.formula_id,
COALESCE(f.formula_name, '') AS formula_name,
DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at,
DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at,
DATE_FORMAT(g.created_at, '%Y-%m-%d %H:%i:%s') AS posted_at,
DATE_FORMAT(g.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at,
CASE g.source_type
WHEN 'receipt' THEN r.receipt_number
WHEN 'payment' THEN p.payment_number
@@ -142,8 +142,8 @@ class GlQueryManager
$sth = $this->pdo->prepare(
"SELECT g.*,
DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at,
DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at_fmt,
DATE_FORMAT(g.created_at, '%Y-%m-%d %H:%i:%s') AS posted_at,
DATE_FORMAT(g.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at_fmt,
DATE_FORMAT(g.journal_date, '%d/%m/%Y') AS journal_date_fmt,
COALESCE(f.formula_name, '') AS formula_name
FROM td_gl g
+1 -1
View File
@@ -230,7 +230,7 @@
$('#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) : '');
$('#due_date').val(inv.due_date ? format_date_input(inv.due_date) : '');
$('#notes').val(inv.notes || '');
// Source link
+2 -2
View File
@@ -274,8 +274,8 @@
$('#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) : '');
$('#po_date').val(po_data.po_date ? format_date_input(po_data.po_date) : '');
$('#expected_date').val(po_data.expected_date ? format_date_input(po_data.expected_date) : '');
$('#department_id').val(po_data.department_id || 0);
$('#notes').val(po_data.notes || '');
$('#discount').val(po_data.discount || 0);
+2 -2
View File
@@ -253,8 +253,8 @@
.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) : '');
$('#request_date').val(request_data.request_date ? format_date_input(request_data.request_date) : '');
$('#required_date').val(request_data.required_date ? format_date_input(request_data.required_date) : '');
$('#department_id').val(request_data.department_id || 0);
$('#notes').val(request_data.notes || '');
$('#discount').val(request_data.discount || 0);
+38
View File
@@ -0,0 +1,38 @@
<?php
session_start();
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/WarehouseManager.php';
// What one location holds — the quantity a stock-out or transfer from it moves
// (StockManager takes the whole approved stock-in row in the bin).
try {
$whMgmt = new WarehouseManager($pdo2, $company_id);
$row = $whMgmt->getBinStock(
(int)($data['warehouse'] ?? 0),
$data['zone'] ?? '',
$data['aisle'] ?? '',
$data['bin'] ?? ''
);
$output = null;
if ($row) {
$sth = $pdo2->prepare("SELECT uom FROM md_product WHERE company_id = :c AND sku = :sku LIMIT 1");
$sth->execute([':c' => $company_id, ':sku' => $row['product_sku']]);
$output = [
'product_sku' => $row['product_sku'],
'lot_number' => $row['lot_number'],
'serial_number' => $row['serial_number'],
'quantity' => (float)$row['in'],
'uom' => (string)($sth->fetchColumn() ?: ''),
];
}
$answer['output'] = $output;
$answer['success'] = 1;
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
?>
+47 -1
View File
@@ -93,6 +93,16 @@
</select>
</div>
<!-- Quantity: a stock-out always takes everything in the location -->
<div class="mb-3 col-lg-6">
<label for="location_quantity" class="form-label">Quantity</label>
<div class="input-group">
<input type="text" id="location_quantity" class="form-control text-end" placeholder="—" disabled>
<span class="input-group-text" id="location_uom" style="min-width:60px;">&nbsp;</span>
</div>
<div class="form-text">The whole quantity in the selected location is taken out.</div>
</div>
<!-- Contact -->
<div class="mb-3 col-lg-6">
<label for="contact" class="form-label">Contact</label>
@@ -159,6 +169,35 @@
}
set_select_value('#bin', data.bin, data.bin);
scan_location_ready = true;
show_location_quantity();
});
}
// Quantity held in the chosen location — the amount this stock-out moves.
function show_location_quantity() {
var bin_val = $('#bin').val();
$('#location_quantity').val('');
$('#location_uom').html('&nbsp;');
if (!$('#warehouse').val() || !bin_val) return;
return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_bin_stock.php',
autoPrepare: true,
checkRequired: 0,
noLoading: true,
queueLock: false,
action: 'read',
data: {
warehouse: $('#warehouse').val(),
zone: advanced ? $('#zone').val() : bin_val,
aisle: advanced ? $('#aisle').val() : bin_val,
bin: bin_val
},
onSuccess: function(res) {
if (!res.output) return;
$('#location_quantity').val(format_number(res.output.quantity, 2));
$('#location_uom').text(res.output.uom || '');
}
});
}
@@ -483,6 +522,8 @@
$('#zone').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
$('#aisle').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
$('#bin').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
$('#location_quantity').val('');
$('#location_uom').html('&nbsp;');
}
@@ -509,7 +550,7 @@
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
.val(data.serial_number || '').prop('disabled', true);
$('#warehouse').val('<?php echo $_GET["wh"];?>').prop('disabled', true);
$('#warehouse').val('<?php echo (int)($_GET["wh"] ?? 0);?>').prop('disabled', true);
function populate_fields() {
$.each(data, function(key, item) {
@@ -522,6 +563,8 @@
.attr('data-id', data.contact_id);
$('#product_name').val(data.product_name);
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
$('#location_quantity').val(format_number(data.quantity, 2));
$('#location_uom').text(data.uom || '');
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
if (data.status == 0) {
@@ -619,6 +662,9 @@
if (advanced) retrieve_bin();
});
// Bin selected → show how much it holds
$('#bin').on('change', show_location_quantity);
<?php } ?>
</script>
+45
View File
@@ -92,6 +92,15 @@
<option value="">Please select bin</option>
</select>
</div>
<!-- Quantity: a transfer always moves everything in the from-location -->
<div class="mb-3 col-lg-3">
<label for="transfer_quantity" class="form-label">Quantity</label>
<div class="input-group">
<input type="text" id="transfer_quantity" class="form-control text-end" placeholder="—" disabled>
<span class="input-group-text" id="transfer_uom" style="min-width:52px;">&nbsp;</span>
</div>
<div class="form-text">Whole location is moved.</div>
</div>
<div class="col-12"><hr class="my-2"></div>
@@ -204,12 +213,41 @@
if (role === 'from') {
scan_from_ready = true;
show_from_quantity();
} else {
scan_to_ready = true;
}
});
}
// Quantity held in the from-location — the amount this transfer moves.
function show_from_quantity() {
var bin_val = $('#bin_from').val();
$('#transfer_quantity').val('');
$('#transfer_uom').html('&nbsp;');
if (!$('#warehouse_from').val() || !bin_val) return;
return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_bin_stock.php',
autoPrepare: true,
checkRequired: 0,
noLoading: true,
queueLock: false,
action: 'read',
data: {
warehouse: $('#warehouse_from').val(),
zone: advanced ? $('#zone_from').val() : bin_val,
aisle: advanced ? $('#aisle_from').val() : bin_val,
bin: bin_val
},
onSuccess: function(res) {
if (!res.output) return;
$('#transfer_quantity').val(format_number(res.output.quantity, 2));
$('#transfer_uom').text(res.output.uom || '');
}
});
}
function same_location_as_from(data) {
return String($('#warehouse_from').val()) === String(data.warehouse_id)
&& String($('#zone_from').val()) === String(data.zone)
@@ -574,6 +612,8 @@
$('#zone_from').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
$('#aisle_from').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
$('#bin_from').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
$('#transfer_quantity').val('');
$('#transfer_uom').html('&nbsp;');
}
@@ -640,6 +680,8 @@
$('#contact').val(data.contact_name)
.attr('secondary', data.contact_name)
.attr('data-id', data.contact_id);
$('#transfer_quantity').val(format_number(data.quantity, 2));
$('#transfer_uom').text(data.uom || '');
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
if (data.status == 0) {
@@ -732,6 +774,9 @@
if (advanced) retrieve_bin('from');
});
// From bin → show how much it holds
$('#bin_from').on('change', show_from_quantity);
// To warehouse → zone (advanced) or bin directly (simple)
$('select[name="warehouse"][role="to"]').on('change', function() {
if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
+1 -1
View File
@@ -187,7 +187,7 @@
document.getElementById('gl_detail_body').innerHTML =
'<div class="row g-3 mb-4">' +
'<div class="col-6"><p class="text-muted small mb-0">Date</p><strong>' + escape_html(h.journal_date_fmt || h.journal_date || '—') + '</strong></div>' +
'<div class="col-6"><p class="text-muted small mb-0">Date</p><strong>' + escape_html(format_date(h.journal_date)) + '</strong></div>' +
'<div class="col-6"><p class="text-muted small mb-0">Period</p><strong>' + escape_html(h.period) + '</strong></div>' +
'<div class="col-6"><p class="text-muted small mb-0">Reference</p><strong>' + escape_html(h.reference || '—') + '</strong></div>' +
'<div class="col-6"><p class="text-muted small mb-0">Source</p><strong>' + escape_html(src_labels[h.source_type] || h.source_type) + '</strong></div>' +
+1 -1
View File
@@ -250,7 +250,7 @@
$('#display_contact').text(inv.contact_name || '—');
$('#display_department').text(get_dept_label(inv.department_id));
$('#display_issued').text(format_date(inv.issued_date) || '—');
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
$('#due_date').val(inv.due_date ? format_date_input(inv.due_date) : '');
$('#notes').val(inv.notes || '');
var gl_type = inv.doc_type === 'credit_note' ? 'sales_credit_note' : 'sales_invoice';
+1 -1
View File
@@ -493,7 +493,7 @@
// Fields
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id);
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
$('#order_date').val(o.order_date ? format_date_input(o.order_date) : '');
$('#department_id').val(o.department_id || 0);
$('#notes').val(o.notes || '');
$('#discount').val(o.discount || 0);
+1 -1
View File
@@ -795,7 +795,7 @@
$('#badge_status').html(return_status_badge(r.status));
$('#return_number_display').text(r.return_number);
$('#return_date').val(r.return_date ? format_date(r.return_date) : '');
$('#return_date').val(r.return_date ? format_date_input(r.return_date) : '');
$('#reason').val(r.reason || '');
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
$('#display_department').text(get_dept_label(r.department_id));
+2 -2
View File
@@ -682,8 +682,8 @@
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id || '');
$('#po_date').val(o.po_date ? format_date(o.po_date) : '');
$('#expected_date').val(o.expected_date ? format_date(o.expected_date) : '');
$('#po_date').val(o.po_date ? format_date_input(o.po_date) : '');
$('#expected_date').val(o.expected_date ? format_date_input(o.expected_date) : '');
$('#warehouse_id').val(o.warehouse_id || '');
$('#department_id').val(o.department_id || 0);
$('#notes').val(o.notes || '');
+1 -1
View File
@@ -226,7 +226,7 @@
$('#display_contact').text(inv.contact_name || '—');
$('#display_department').text(get_dept_label(inv.department_id));
$('#display_issued').text(format_date(inv.issued_date) || '—');
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
$('#due_date').val(inv.due_date ? format_date_input(inv.due_date) : '');
$('#notes').val(inv.notes || '');
// Source link
+1 -1
View File
@@ -432,7 +432,7 @@
$('#badge_status').html(return_status_badge(r.status));
$('#badge_fulfillment').html(fulfillment_status_badge(r.fulfillment_status));
$('#return_number_display').text(r.return_number);
$('#return_date').val(r.return_date ? format_date(r.return_date) : '');
$('#return_date').val(r.return_date ? format_date_input(r.return_date) : '');
$('#reason').val(r.reason || '');
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
$('#display_department').text(get_dept_label(r.department_id));
+1 -1
View File
@@ -207,7 +207,7 @@
$('#display_contact').html(display_text(inv.contact_name));
$('#display_issued').html(inv.issued_date ? format_date(inv.issued_date) : '<span class="text-muted">—</span>');
$('#display_due').html(inv.due_date ? format_date(inv.due_date) : '<span class="text-muted">—</span>');
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
$('#due_date').val(inv.due_date ? format_date_input(inv.due_date) : '');
$('#display_notes').html(inv.notes ? escape_html(inv.notes).replace(/\n/g, '<br>') : '<span class="text-muted">—</span>');
var rows = '';
+1 -1
View File
@@ -356,7 +356,7 @@
$('#order_number_display').text(o.order_number);
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id || 0);
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
$('#order_date').val(o.order_date ? format_date_input(o.order_date) : '');
$('#notes').val(o.notes || '');
$('#discount').val(o.discount || 0);
$('#tax_adjustment').val(o.tax_adjustment || 0);
+2 -2
View File
@@ -379,8 +379,8 @@
$('#contact').val(q.contact_name || '');
$('#contact_id').val(q.contact_id || 0);
$('#quotation_date').val(q.quotation_date ? format_date(q.quotation_date) : '');
$('#valid_until').val(q.valid_until ? format_date(q.valid_until) : '');
$('#quotation_date').val(q.quotation_date ? format_date_input(q.quotation_date) : '');
$('#valid_until').val(q.valid_until ? format_date_input(q.valid_until) : '');
$('#department_id').val(q.department_id || 0);
$('#notes').val(q.notes || '');
$('#discount').val(parseFloat(q.discount) || 0);
+1 -3
View File
@@ -257,9 +257,7 @@
const license_badge = license_html(u.license);
const access_badge = app_access_html(u.app_access);
const joined = u.created_at
? new Date(u.created_at).toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'})
: '—';
const joined = u.created_at ? format_date(String(u.created_at).split(' ')[0]) : '—';
const is_pending = u.status === 'pending' && u.is_pending_invite == 1;