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, use product names and supplier batches, and add demo_seed_assets.php.
This commit is contained in:
@@ -523,7 +523,7 @@
|
|||||||
|
|
||||||
var extra_fields = (h.source_type === 'manual' || String(h.source_type).indexOf('asset_') === 0)
|
var extra_fields = (h.source_type === 'manual' || String(h.source_type).indexOf('asset_') === 0)
|
||||||
? '<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">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">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">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>' +
|
'<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>' +
|
||||||
|
|||||||
@@ -329,7 +329,7 @@
|
|||||||
invoice_line = res.output;
|
invoice_line = res.output;
|
||||||
var l = invoice_line;
|
var l = invoice_line;
|
||||||
$('#asset_name').val(l.product_name || l.product_sku);
|
$('#asset_name').val(l.product_name || l.product_sku);
|
||||||
$('#acquisition_date, #in_service_date').val(format_date(l.issued_date));
|
$('#acquisition_date, #in_service_date').val(format_date_input(l.issued_date));
|
||||||
$('#cost').val(Math.max(parseFloat(l.remaining) || 0, 0).toFixed(2));
|
$('#cost').val(Math.max(parseFloat(l.remaining) || 0, 0).toFixed(2));
|
||||||
$('#clearing_account_code').html(account_options(accounts, [], l.clearing_account_code, '— None —'));
|
$('#clearing_account_code').html(account_options(accounts, [], l.clearing_account_code, '— None —'));
|
||||||
load_departments('department_id', l.department_id);
|
load_departments('department_id', l.department_id);
|
||||||
@@ -360,8 +360,8 @@
|
|||||||
fill_category_select(a.category_id);
|
fill_category_select(a.category_id);
|
||||||
update_class_hint();
|
update_class_hint();
|
||||||
$('#clearing_account_code').html(account_options(accounts, [], a.clearing_account_code, '— None —'));
|
$('#clearing_account_code').html(account_options(accounts, [], a.clearing_account_code, '— None —'));
|
||||||
$('#acquisition_date').val(format_date(a.acquisition_date));
|
$('#acquisition_date').val(format_date_input(a.acquisition_date));
|
||||||
$('#in_service_date').val(format_date(a.in_service_date));
|
$('#in_service_date').val(format_date_input(a.in_service_date));
|
||||||
$('#cost').val(parseFloat(a.cost).toFixed(2));
|
$('#cost').val(parseFloat(a.cost).toFixed(2));
|
||||||
$('#salvage_value').val(parseFloat(a.salvage_value).toFixed(2));
|
$('#salvage_value').val(parseFloat(a.salvage_value).toFixed(2));
|
||||||
$('#useful_life_months').val(a.useful_life_months);
|
$('#useful_life_months').val(a.useful_life_months);
|
||||||
|
|||||||
+11
-2
@@ -995,15 +995,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
/**
|
/**
|
||||||
* Helper function to format date strings (assuming input is in ISO format)
|
* 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) {
|
function format_date(iso_string) {
|
||||||
if (!iso_string) return '—';
|
if (!iso_string) return '—';
|
||||||
var split = iso_string.split(" ");
|
var split = String(iso_string).split(" ");
|
||||||
var datePart = split[0].split("-");
|
var datePart = split[0].split("-");
|
||||||
if (datePart.length !== 3) return iso_string;
|
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;
|
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) {
|
function to_iso_date(dateStr) {
|
||||||
if (!dateStr) return null;
|
if (!dateStr) return null;
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ class StockManager {
|
|||||||
public function getStockList(int $warehouse_id, string $type): array
|
public function getStockList(int $warehouse_id, string $type): array
|
||||||
{
|
{
|
||||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
$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();
|
$stock_ref = $this->stockReferenceSql();
|
||||||
|
|
||||||
// Transfer list: show only the outbound side (out > 0) to avoid duplicate display
|
// 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 and identifiers come from the existing stock_in row (immutable).
|
||||||
$quantity = (int)$source_stock['in'];
|
// `in` is decimal(18,4) — an int cast would drop fractional quantities.
|
||||||
|
$quantity = (float)$source_stock['in'];
|
||||||
$ref_id = (int)$source_stock['id'];
|
$ref_id = (int)$source_stock['id'];
|
||||||
$lot_number = $source_stock['lot_number'] ?? null;
|
$lot_number = $source_stock['lot_number'] ?? null;
|
||||||
$serial_number = $source_stock['serial_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 and identifiers come from the source stock_in row (immutable).
|
||||||
$quantity = (int)$source_stock['in'];
|
// `in` is decimal(18,4) — an int cast would drop fractional quantities.
|
||||||
|
$quantity = (float)$source_stock['in'];
|
||||||
$lot_number = $source_stock['lot_number'] ?? null;
|
$lot_number = $source_stock['lot_number'] ?? null;
|
||||||
$serial_number = $source_stock['serial_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_code, '') AS dept_code,
|
||||||
COALESCE(d.dept_name, '') AS dept_name,
|
COALESCE(d.dept_name, '') AS dept_name,
|
||||||
COALESCE(g.journal_date, DATE(g.created_at)) AS entry_date,
|
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.debit,
|
||||||
i.credit,
|
i.credit,
|
||||||
COALESCE(i.description, '') AS line_description
|
COALESCE(i.description, '') AS line_description
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ class GlQueryManager
|
|||||||
g.current_version,
|
g.current_version,
|
||||||
g.formula_id,
|
g.formula_id,
|
||||||
COALESCE(f.formula_name, '') AS formula_name,
|
COALESCE(f.formula_name, '') AS formula_name,
|
||||||
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,
|
||||||
DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at,
|
DATE_FORMAT(g.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at,
|
||||||
CASE g.source_type
|
CASE g.source_type
|
||||||
WHEN 'receipt' THEN r.receipt_number
|
WHEN 'receipt' THEN r.receipt_number
|
||||||
WHEN 'payment' THEN p.payment_number
|
WHEN 'payment' THEN p.payment_number
|
||||||
@@ -160,8 +160,8 @@ class GlQueryManager
|
|||||||
|
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT g.*,
|
"SELECT g.*,
|
||||||
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,
|
||||||
DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at_fmt,
|
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,
|
DATE_FORMAT(g.journal_date, '%d/%m/%Y') AS journal_date_fmt,
|
||||||
COALESCE(f.formula_name, '') AS formula_name
|
COALESCE(f.formula_name, '') AS formula_name
|
||||||
FROM td_gl g
|
FROM td_gl g
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class AssetManager
|
|||||||
{
|
{
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT id, reference, journal_date, period, current_version,
|
"SELECT id, reference, journal_date, period, current_version,
|
||||||
DATE_FORMAT(created_at, '%d/%m/%Y %H:%i') AS posted_at
|
DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS posted_at
|
||||||
FROM td_gl WHERE company_id = :cid AND source_type = :type AND source_id = :id LIMIT 1"
|
FROM td_gl WHERE company_id = :cid AND source_type = :type AND source_id = :id LIMIT 1"
|
||||||
);
|
);
|
||||||
$sth->execute([':cid' => $this->companyId, ':type' => $source_type, ':id' => $source_id]);
|
$sth->execute([':cid' => $this->companyId, ':type' => $source_type, ':id' => $source_id]);
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class AssetRunManager
|
|||||||
|
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT r.*,
|
"SELECT r.*,
|
||||||
DATE_FORMAT(r.created_at, '%d/%m/%Y %H:%i') AS created_at_fmt,
|
DATE_FORMAT(r.created_at, '%Y-%m-%d %H:%i:%s') AS created_at_fmt,
|
||||||
IF(g.id IS NULL, 0, 1) AS gl_status,
|
IF(g.id IS NULL, 0, 1) AS gl_status,
|
||||||
COALESCE(g.id, 0) AS gl_id
|
COALESCE(g.id, 0) AS gl_id
|
||||||
FROM td_asset_run r
|
FROM td_asset_run r
|
||||||
|
|||||||
@@ -294,7 +294,7 @@
|
|||||||
$('#badge_status').html(invoice_status_badge(inv.status, inv.due_date));
|
$('#badge_status').html(invoice_status_badge(inv.status, inv.due_date));
|
||||||
$('#display_contact').text(inv.contact_name || '—');
|
$('#display_contact').text(inv.contact_name || '—');
|
||||||
$('#display_issued').text(format_date(inv.issued_date) || '—');
|
$('#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 || '');
|
$('#notes').val(inv.notes || '');
|
||||||
|
|
||||||
// Source link
|
// Source link
|
||||||
|
|||||||
@@ -274,8 +274,8 @@
|
|||||||
$('#po_number_display').text(po_data.po_number || '');
|
$('#po_number_display').text(po_data.po_number || '');
|
||||||
$('#contact').val(po_data.contact_name || '');
|
$('#contact').val(po_data.contact_name || '');
|
||||||
$('#contact_id').val(po_data.contact_id || '');
|
$('#contact_id').val(po_data.contact_id || '');
|
||||||
$('#po_date').val(po_data.po_date ? format_date(po_data.po_date) : '');
|
$('#po_date').val(po_data.po_date ? format_date_input(po_data.po_date) : '');
|
||||||
$('#expected_date').val(po_data.expected_date ? format_date(po_data.expected_date) : '');
|
$('#expected_date').val(po_data.expected_date ? format_date_input(po_data.expected_date) : '');
|
||||||
$('#department_id').val(po_data.department_id || 0);
|
$('#department_id').val(po_data.department_id || 0);
|
||||||
$('#notes').val(po_data.notes || '');
|
$('#notes').val(po_data.notes || '');
|
||||||
$('#discount').val(po_data.discount || 0);
|
$('#discount').val(po_data.discount || 0);
|
||||||
|
|||||||
@@ -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>' : '');
|
.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').val(request_data.contact_name || '');
|
||||||
$('#contact_id').val(request_data.contact_id || 0);
|
$('#contact_id').val(request_data.contact_id || 0);
|
||||||
$('#request_date').val(request_data.request_date ? format_date(request_data.request_date) : '');
|
$('#request_date').val(request_data.request_date ? format_date_input(request_data.request_date) : '');
|
||||||
$('#required_date').val(request_data.required_date ? format_date(request_data.required_date) : '');
|
$('#required_date').val(request_data.required_date ? format_date_input(request_data.required_date) : '');
|
||||||
$('#department_id').val(request_data.department_id || 0);
|
$('#department_id').val(request_data.department_id || 0);
|
||||||
$('#notes').val(request_data.notes || '');
|
$('#notes').val(request_data.notes || '');
|
||||||
$('#discount').val(request_data.discount || 0);
|
$('#discount').val(request_data.discount || 0);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
?>
|
||||||
@@ -93,6 +93,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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;"> </span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">The whole quantity in the selected location is taken out.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Contact -->
|
<!-- Contact -->
|
||||||
<div class="mb-3 col-lg-6">
|
<div class="mb-3 col-lg-6">
|
||||||
<label for="contact" class="form-label">Contact</label>
|
<label for="contact" class="form-label">Contact</label>
|
||||||
@@ -159,6 +169,35 @@
|
|||||||
}
|
}
|
||||||
set_select_value('#bin', data.bin, data.bin);
|
set_select_value('#bin', data.bin, data.bin);
|
||||||
scan_location_ready = true;
|
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(' ');
|
||||||
|
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);
|
$('#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);
|
$('#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);
|
$('#bin').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
|
||||||
|
$('#location_quantity').val('');
|
||||||
|
$('#location_uom').html(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -509,7 +550,7 @@
|
|||||||
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
|
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
|
||||||
.val(data.serial_number || '').prop('disabled', true);
|
.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() {
|
function populate_fields() {
|
||||||
$.each(data, function(key, item) {
|
$.each(data, function(key, item) {
|
||||||
@@ -522,6 +563,8 @@
|
|||||||
.attr('data-id', data.contact_id);
|
.attr('data-id', data.contact_id);
|
||||||
$('#product_name').val(data.product_name);
|
$('#product_name').val(data.product_name);
|
||||||
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
|
$('#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=submit]').text('Update');
|
||||||
$('button[type=reset]').hide();
|
$('button[type=reset]').hide();
|
||||||
if (data.status == 0) {
|
if (data.status == 0) {
|
||||||
@@ -619,6 +662,9 @@
|
|||||||
if (advanced) retrieve_bin();
|
if (advanced) retrieve_bin();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bin selected → show how much it holds
|
||||||
|
$('#bin').on('change', show_location_quantity);
|
||||||
|
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,15 @@
|
|||||||
<option value="">Please select bin</option>
|
<option value="">Please select bin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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;"> </span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Whole location is moved.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12"><hr class="my-2"></div>
|
<div class="col-12"><hr class="my-2"></div>
|
||||||
|
|
||||||
@@ -204,12 +213,41 @@
|
|||||||
|
|
||||||
if (role === 'from') {
|
if (role === 'from') {
|
||||||
scan_from_ready = true;
|
scan_from_ready = true;
|
||||||
|
show_from_quantity();
|
||||||
} else {
|
} else {
|
||||||
scan_to_ready = true;
|
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(' ');
|
||||||
|
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) {
|
function same_location_as_from(data) {
|
||||||
return String($('#warehouse_from').val()) === String(data.warehouse_id)
|
return String($('#warehouse_from').val()) === String(data.warehouse_id)
|
||||||
&& String($('#zone_from').val()) === String(data.zone)
|
&& 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);
|
$('#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);
|
$('#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);
|
$('#bin_from').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
|
||||||
|
$('#transfer_quantity').val('');
|
||||||
|
$('#transfer_uom').html(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -640,6 +680,8 @@
|
|||||||
$('#contact').val(data.contact_name)
|
$('#contact').val(data.contact_name)
|
||||||
.attr('secondary', data.contact_name)
|
.attr('secondary', data.contact_name)
|
||||||
.attr('data-id', data.contact_id);
|
.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=submit]').text('Update');
|
||||||
$('button[type=reset]').hide();
|
$('button[type=reset]').hide();
|
||||||
if (data.status == 0) {
|
if (data.status == 0) {
|
||||||
@@ -732,6 +774,9 @@
|
|||||||
if (advanced) retrieve_bin('from');
|
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)
|
// To warehouse → zone (advanced) or bin directly (simple)
|
||||||
$('select[name="warehouse"][role="to"]').on('change', function() {
|
$('select[name="warehouse"][role="to"]').on('change', function() {
|
||||||
if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
|
if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
|
|
||||||
document.getElementById('gl_detail_body').innerHTML =
|
document.getElementById('gl_detail_body').innerHTML =
|
||||||
'<div class="row g-3 mb-4">' +
|
'<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">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">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>' +
|
'<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>' +
|
||||||
|
|||||||
@@ -250,7 +250,7 @@
|
|||||||
$('#display_contact').text(inv.contact_name || '—');
|
$('#display_contact').text(inv.contact_name || '—');
|
||||||
$('#display_department').text(get_dept_label(inv.department_id));
|
$('#display_department').text(get_dept_label(inv.department_id));
|
||||||
$('#display_issued').text(format_date(inv.issued_date) || '—');
|
$('#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 || '');
|
$('#notes').val(inv.notes || '');
|
||||||
|
|
||||||
var gl_type = inv.doc_type === 'credit_note' ? 'sales_credit_note' : 'sales_invoice';
|
var gl_type = inv.doc_type === 'credit_note' ? 'sales_credit_note' : 'sales_invoice';
|
||||||
|
|||||||
@@ -493,7 +493,7 @@
|
|||||||
// Fields
|
// Fields
|
||||||
$('#contact').val(o.contact_name || '');
|
$('#contact').val(o.contact_name || '');
|
||||||
$('#contact_id').val(o.contact_id);
|
$('#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);
|
$('#department_id').val(o.department_id || 0);
|
||||||
$('#notes').val(o.notes || '');
|
$('#notes').val(o.notes || '');
|
||||||
$('#discount').val(o.discount || 0);
|
$('#discount').val(o.discount || 0);
|
||||||
|
|||||||
@@ -795,7 +795,7 @@
|
|||||||
$('#badge_status').html(return_status_badge(r.status));
|
$('#badge_status').html(return_status_badge(r.status));
|
||||||
$('#return_number_display').text(r.return_number);
|
$('#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 || '');
|
$('#reason').val(r.reason || '');
|
||||||
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
||||||
$('#display_department').text(get_dept_label(r.department_id));
|
$('#display_department').text(get_dept_label(r.department_id));
|
||||||
|
|||||||
@@ -682,8 +682,8 @@
|
|||||||
|
|
||||||
$('#contact').val(o.contact_name || '');
|
$('#contact').val(o.contact_name || '');
|
||||||
$('#contact_id').val(o.contact_id || '');
|
$('#contact_id').val(o.contact_id || '');
|
||||||
$('#po_date').val(o.po_date ? format_date(o.po_date) : '');
|
$('#po_date').val(o.po_date ? format_date_input(o.po_date) : '');
|
||||||
$('#expected_date').val(o.expected_date ? format_date(o.expected_date) : '');
|
$('#expected_date').val(o.expected_date ? format_date_input(o.expected_date) : '');
|
||||||
$('#warehouse_id').val(o.warehouse_id || '');
|
$('#warehouse_id').val(o.warehouse_id || '');
|
||||||
$('#department_id').val(o.department_id || 0);
|
$('#department_id').val(o.department_id || 0);
|
||||||
$('#notes').val(o.notes || '');
|
$('#notes').val(o.notes || '');
|
||||||
|
|||||||
@@ -226,7 +226,7 @@
|
|||||||
$('#display_contact').text(inv.contact_name || '—');
|
$('#display_contact').text(inv.contact_name || '—');
|
||||||
$('#display_department').text(get_dept_label(inv.department_id));
|
$('#display_department').text(get_dept_label(inv.department_id));
|
||||||
$('#display_issued').text(format_date(inv.issued_date) || '—');
|
$('#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 || '');
|
$('#notes').val(inv.notes || '');
|
||||||
|
|
||||||
// Source link
|
// Source link
|
||||||
|
|||||||
@@ -432,7 +432,7 @@
|
|||||||
$('#badge_status').html(return_status_badge(r.status));
|
$('#badge_status').html(return_status_badge(r.status));
|
||||||
$('#badge_fulfillment').html(fulfillment_status_badge(r.fulfillment_status));
|
$('#badge_fulfillment').html(fulfillment_status_badge(r.fulfillment_status));
|
||||||
$('#return_number_display').text(r.return_number);
|
$('#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 || '');
|
$('#reason').val(r.reason || '');
|
||||||
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
$('#tax_adjustment').val(parseFloat(r.tax_adjustment || 0).toFixed(2));
|
||||||
$('#display_department').text(get_dept_label(r.department_id));
|
$('#display_department').text(get_dept_label(r.department_id));
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
$('#display_contact').html(display_text(inv.contact_name));
|
$('#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_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>');
|
$('#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>');
|
$('#display_notes').html(inv.notes ? escape_html(inv.notes).replace(/\n/g, '<br>') : '<span class="text-muted">—</span>');
|
||||||
|
|
||||||
var rows = '';
|
var rows = '';
|
||||||
|
|||||||
@@ -356,7 +356,7 @@
|
|||||||
$('#order_number_display').text(o.order_number);
|
$('#order_number_display').text(o.order_number);
|
||||||
$('#contact').val(o.contact_name || '');
|
$('#contact').val(o.contact_name || '');
|
||||||
$('#contact_id').val(o.contact_id || 0);
|
$('#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 || '');
|
$('#notes').val(o.notes || '');
|
||||||
$('#discount').val(o.discount || 0);
|
$('#discount').val(o.discount || 0);
|
||||||
$('#tax_adjustment').val(o.tax_adjustment || 0);
|
$('#tax_adjustment').val(o.tax_adjustment || 0);
|
||||||
|
|||||||
@@ -379,8 +379,8 @@
|
|||||||
|
|
||||||
$('#contact').val(q.contact_name || '');
|
$('#contact').val(q.contact_name || '');
|
||||||
$('#contact_id').val(q.contact_id || 0);
|
$('#contact_id').val(q.contact_id || 0);
|
||||||
$('#quotation_date').val(q.quotation_date ? format_date(q.quotation_date) : '');
|
$('#quotation_date').val(q.quotation_date ? format_date_input(q.quotation_date) : '');
|
||||||
$('#valid_until').val(q.valid_until ? format_date(q.valid_until) : '');
|
$('#valid_until').val(q.valid_until ? format_date_input(q.valid_until) : '');
|
||||||
$('#department_id').val(q.department_id || 0);
|
$('#department_id').val(q.department_id || 0);
|
||||||
$('#notes').val(q.notes || '');
|
$('#notes').val(q.notes || '');
|
||||||
$('#discount').val(parseFloat(q.discount) || 0);
|
$('#discount').val(parseFloat(q.discount) || 0);
|
||||||
|
|||||||
@@ -257,9 +257,7 @@
|
|||||||
const license_badge = license_html(u.license);
|
const license_badge = license_html(u.license);
|
||||||
const access_badge = app_access_html(u.app_access);
|
const access_badge = app_access_html(u.app_access);
|
||||||
|
|
||||||
const joined = u.created_at
|
const joined = u.created_at ? format_date(String(u.created_at).split(' ')[0]) : '—';
|
||||||
? new Date(u.created_at).toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'})
|
|
||||||
: '—';
|
|
||||||
|
|
||||||
const is_pending = u.status === 'pending' && u.is_pending_invite == 1;
|
const is_pending = u.status === 'pending' && u.is_pending_invite == 1;
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -532,9 +532,11 @@ foreach ($stockout_defs as $so) {
|
|||||||
$qty = $so['qty'];
|
$qty = $so['qty'];
|
||||||
$cost = $p['cost'];
|
$cost = $p['cost'];
|
||||||
$uom = $p['uom'];
|
$uom = $p['uom'];
|
||||||
|
// The batch is bought in from a supplier; only the stock-out goes to the customer.
|
||||||
|
$supplier_id = $supplier_ids[0];
|
||||||
|
|
||||||
dbTransaction($pdo2, function ($pdo2) use (
|
dbTransaction($pdo2, function ($pdo2) use (
|
||||||
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $sku, $qty, $cost, $customer_id,
|
$stockMgmt, $whMgmt, $company_id, $main_wh_id, $sku, $qty, $cost, $customer_id, $supplier_id,
|
||||||
$logging, $dispatch_in_marker, $dispatch_out_marker
|
$logging, $dispatch_in_marker, $dispatch_out_marker
|
||||||
) {
|
) {
|
||||||
$bin = findFreeBin($pdo2, $company_id, $main_wh_id);
|
$bin = findFreeBin($pdo2, $company_id, $main_wh_id);
|
||||||
@@ -549,7 +551,7 @@ foreach ($stockout_defs as $so) {
|
|||||||
'zone' => $bin,
|
'zone' => $bin,
|
||||||
'aisle' => $bin,
|
'aisle' => $bin,
|
||||||
'bin' => $bin,
|
'bin' => $bin,
|
||||||
'contact_id' => $customer_id,
|
'contact_id' => $supplier_id,
|
||||||
'description' => $dispatch_in_marker,
|
'description' => $dispatch_in_marker,
|
||||||
], $logging, $in_uuid);
|
], $logging, $in_uuid);
|
||||||
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
|
$stockMgmt->approveStock($stock_id, $main_wh_id, 'in', $whMgmt);
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Demo Data Seeder — Asset Management
|
||||||
|
* Run after demo_seed.php and demo_seed_transactions.php: php demo_seed_assets.php
|
||||||
|
*
|
||||||
|
* Seeds the Asset Management app through the same classes its pages and the
|
||||||
|
* Batch GL Entries page use:
|
||||||
|
* 1. GL accounts for fixed assets, and asset categories (AssetCategoryManager)
|
||||||
|
* 2. A purchase of a laptop fleet + an ERP licence: PO -> confirm -> purchase
|
||||||
|
* invoice -> issue -> GL post (PurchaseOrderManager, InvoiceManager), the
|
||||||
|
* only way a purchase invoice exists in the app
|
||||||
|
* 3. Assets capitalized from those invoice lines, plus two vehicles bought
|
||||||
|
* before the system was used and already on the books (AssetManager)
|
||||||
|
* 4. Depreciation runs up to last month, the sale of the forklift today, then
|
||||||
|
* this month's depreciation and amortization runs (AssetRunManager)
|
||||||
|
* 5. GL posting of the purchase invoice, capitalizations, the disposal and every
|
||||||
|
* run before this month (AssetPosting + GlManager, as
|
||||||
|
* accounting/api/engine/post_gl_entry.php does) — this month's runs stay
|
||||||
|
* unposted so Batch GL Entries has Asset Management work to show.
|
||||||
|
*
|
||||||
|
* Dates are relative to today: the app stamps documents with the current date.
|
||||||
|
*
|
||||||
|
* Safe to re-run: every step checks what already exists.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Run via CLI only: php demo_seed_assets.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION = [];
|
||||||
|
|
||||||
|
require_once __DIR__ . '/app/config.php';
|
||||||
|
require_once __DIR__ . '/app/dbconn.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/db_helpers.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/StockManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/ProductManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/PurchaseOrderManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/InvoiceManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_ac/ChartOfAccounts.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_ac/PostingWindowGuard.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_ac/GlManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/BasePosting.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_ac/posting/PurchaseInvoicePosting.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_as/AssetManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_as/AssetRunManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes_as/AssetPosting.php';
|
||||||
|
|
||||||
|
function info(string $msg): void { echo "\033[36m •\033[0m {$msg}\n"; }
|
||||||
|
function ok(string $msg): void { echo "\033[32m ✓\033[0m {$msg}\n"; }
|
||||||
|
function skip(string $msg): void { echo "\033[33m –\033[0m {$msg}\n"; }
|
||||||
|
|
||||||
|
echo "\n\033[1m=== brnwms Demo Asset Management Seeder ===\033[0m\n\n";
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 0. Resolve what the earlier seeders created
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
$sth = $pdo1->prepare("SELECT company_id FROM company_list WHERE channel_name = 'demo' LIMIT 1");
|
||||||
|
$sth->execute();
|
||||||
|
$company_id = (int)($sth->fetchColumn() ?: 0);
|
||||||
|
if (!$company_id) exit("ERROR: demo company not found — run demo_seed.php first.\n");
|
||||||
|
|
||||||
|
$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = 'admin' LIMIT 1");
|
||||||
|
$sth->execute();
|
||||||
|
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
|
||||||
|
|
||||||
|
$lookup = function (string $sql, array $params) use ($pdo2): int {
|
||||||
|
$sth = $pdo2->prepare($sql);
|
||||||
|
$sth->execute($params);
|
||||||
|
return (int)($sth->fetchColumn() ?: 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$main_wh = $lookup("SELECT id FROM md_warehouse WHERE company_id = :c AND warehouse_name = 'Main Warehouse' LIMIT 1", [':c' => $company_id]);
|
||||||
|
$supplier_id = $lookup("SELECT id FROM md_contact WHERE company_id = :c AND contact_name = 'Asia Electronics Wholesale' LIMIT 1", [':c' => $company_id]);
|
||||||
|
$wh_dept_id = $lookup("SELECT id FROM md_department WHERE company_id = :c AND dept_code = 'WH' LIMIT 1", [':c' => $company_id]);
|
||||||
|
if (!$main_wh || !$supplier_id || !$wh_dept_id) {
|
||||||
|
exit("ERROR: warehouse, supplier or WH department missing — run demo_seed.php and demo_seed_transactions.php first.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$coa = new ChartOfAccounts($pdo2, $company_id);
|
||||||
|
foreach (['1000', '4000', '5000'] as $code) {
|
||||||
|
if (!$coa->getByCode($code)) exit("ERROR: account {$code} missing — run demo_seed_transactions.php first.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_assets'];
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$this_period = date('Y-m');
|
||||||
|
$last_period = DepreciationCalculator::addMonths($this_period, -1);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 1. Accounts + asset categories
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo "--- Asset accounts ---\n";
|
||||||
|
|
||||||
|
$account_defs = [
|
||||||
|
['code' => '1500', 'name' => 'Equipment and Vehicles', 'type' => 'asset'],
|
||||||
|
['code' => '1590', 'name' => 'Accumulated Depreciation', 'type' => 'asset'],
|
||||||
|
['code' => '1600', 'name' => 'Software and Licences', 'type' => 'asset'],
|
||||||
|
['code' => '1690', 'name' => 'Accumulated Amortization', 'type' => 'asset'],
|
||||||
|
['code' => '4900', 'name' => 'Gain on Asset Disposal', 'type' => 'revenue'],
|
||||||
|
['code' => '6100', 'name' => 'Depreciation Expense', 'type' => 'expense'],
|
||||||
|
['code' => '6200', 'name' => 'Amortization Expense', 'type' => 'expense'],
|
||||||
|
['code' => '6900', 'name' => 'Loss on Asset Disposal', 'type' => 'expense'],
|
||||||
|
];
|
||||||
|
foreach ($account_defs as $a) {
|
||||||
|
if ($coa->getByCode($a['code'])) {
|
||||||
|
skip("Account {$a['code']} — {$a['name']} already exists");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$coa->create([
|
||||||
|
'account_code' => $a['code'], 'account_name' => $a['name'], 'account_type' => $a['type'],
|
||||||
|
'account_category' => '', 'parent_code' => null, 'is_posting' => 1, 'status' => 1,
|
||||||
|
]);
|
||||||
|
ok("Created account {$a['code']} — {$a['name']} ({$a['type']})");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n--- Asset categories ---\n";
|
||||||
|
|
||||||
|
$category_defs = [
|
||||||
|
['code' => 'IT', 'name' => 'Computer Equipment', 'class' => 'tangible', 'life' => 36, 'cost' => '1500', 'accum' => '1590', 'expense' => '6100'],
|
||||||
|
['code' => 'VEH', 'name' => 'Vehicles and Forklifts', 'class' => 'tangible', 'life' => 60, 'cost' => '1500', 'accum' => '1590', 'expense' => '6100'],
|
||||||
|
['code' => 'SW', 'name' => 'Software Licences', 'class' => 'intangible', 'life' => 24, 'cost' => '1600', 'accum' => '1690', 'expense' => '6200'],
|
||||||
|
];
|
||||||
|
$category_ids = [];
|
||||||
|
foreach ($category_defs as $c) {
|
||||||
|
$id = $lookup("SELECT id FROM md_asset_category WHERE company_id = :c AND category_code = :code LIMIT 1", [':c' => $company_id, ':code' => $c['code']]);
|
||||||
|
if ($id) {
|
||||||
|
skip("Category {$c['code']} already exists (id={$id})");
|
||||||
|
} else {
|
||||||
|
$id = (new AssetCategoryManager($pdo2, $company_id))->save([
|
||||||
|
'category_code' => $c['code'], 'category_name' => $c['name'], 'asset_class' => $c['class'],
|
||||||
|
'useful_life_months' => $c['life'], 'status' => 1, 'description' => '',
|
||||||
|
'cost_account_code' => $c['cost'], 'accum_account_code' => $c['accum'], 'expense_account_code' => $c['expense'],
|
||||||
|
'gain_account_code' => '4900', 'loss_account_code' => '6900',
|
||||||
|
]);
|
||||||
|
ok("Created category {$c['code']} — {$c['name']} ({$c['class']}, {$c['life']} months)");
|
||||||
|
}
|
||||||
|
$category_ids[$c['code']] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 2. Purchase: PO -> confirm -> purchase invoice -> issue -> GL
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo "\n--- Asset purchase (PO -> purchase invoice) ---\n";
|
||||||
|
|
||||||
|
$prodMgmt = new ProductManager($pdo2, $company_id);
|
||||||
|
|
||||||
|
$cat_id = $lookup("SELECT id FROM md_product_category WHERE company_id = :c AND slug = 'fixed-assets' LIMIT 1", [':c' => $company_id]);
|
||||||
|
if (!$cat_id) {
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new ProductManager($pdo, $company_id))->saveCategory([
|
||||||
|
'id' => 0, 'category' => 'Fixed Assets', 'slug' => 'fixed-assets', 'description' => '', 'status' => 1,
|
||||||
|
], $logging));
|
||||||
|
$cat_id = $lookup("SELECT id FROM md_product_category WHERE company_id = :c AND slug = 'fixed-assets' LIMIT 1", [':c' => $company_id]);
|
||||||
|
ok("Created product category 'Fixed Assets' (id={$cat_id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
$asset_product_defs = [
|
||||||
|
['sku' => 'FA-LAPTOP', 'name' => 'Laptop - Dell Latitude 5440', 'price' => 32000],
|
||||||
|
['sku' => 'FA-ERP', 'name' => 'ERP Software Licence (2 years)', 'price' => 48000],
|
||||||
|
];
|
||||||
|
$products = [];
|
||||||
|
foreach ($asset_product_defs as $p) {
|
||||||
|
$id = $lookup("SELECT id FROM md_product WHERE company_id = :c AND sku = :s LIMIT 1", [':c' => $company_id, ':s' => $p['sku']]);
|
||||||
|
if ($id) {
|
||||||
|
skip("Product {$p['sku']} already exists");
|
||||||
|
} else {
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new ProductManager($pdo, $company_id))->saveProduct([
|
||||||
|
'id' => 0, 'product_name' => $p['name'], 'sku' => $p['sku'], 'barcode' => '', 'uom' => 'pcs',
|
||||||
|
'price' => $p['price'], 'cost_price' => $p['price'], 'min_stock' => 0, 'reorder_point' => 0,
|
||||||
|
'category' => $cat_id, 'description' => 'Bought as a fixed asset (demo seed)', 'status' => 1,
|
||||||
|
], $logging, ''));
|
||||||
|
$id = $lookup("SELECT id FROM md_product WHERE company_id = :c AND sku = :s LIMIT 1", [':c' => $company_id, ':s' => $p['sku']]);
|
||||||
|
// Same accounts as the other demo products: the purchase posts to 5000, and the
|
||||||
|
// capitalization entry reclassifies it from there to the asset account.
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new ProductManager($pdo, $company_id))->updateAccountMapping($id, '4000', '5000', $logging));
|
||||||
|
ok("Created product {$p['sku']} — {$p['name']} (sales 4000, purchase 5000)");
|
||||||
|
}
|
||||||
|
$products[$p['sku']] = $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
$po_marker = 'Demo seed asset purchase';
|
||||||
|
$po_id = $lookup("SELECT id FROM td_purchase_order WHERE company_id = :c AND notes = :n LIMIT 1", [':c' => $company_id, ':n' => $po_marker]);
|
||||||
|
if ($po_id) {
|
||||||
|
skip("Asset purchase PO already exists (id={$po_id})");
|
||||||
|
} else {
|
||||||
|
$po_items = [];
|
||||||
|
foreach ([['FA-LAPTOP', 3], ['FA-ERP', 1]] as $i => [$sku, $qty]) {
|
||||||
|
$total = round($qty * $products[$sku]['price'], 4);
|
||||||
|
$po_items[] = [
|
||||||
|
'item_id' => $i + 1, 'product_sku' => $sku, 'product_name' => $products[$sku]['name'],
|
||||||
|
'quantity' => $qty, 'unit_price' => $products[$sku]['price'], 'total_price' => $total,
|
||||||
|
'tax_rate' => 7.0, 'tax_amount' => round($total * 0.07, 4),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$po_id = dbTransaction($pdo2, function ($pdo) use ($company_id, $supplier_id, $wh_dept_id, $main_wh, $po_items, $today, $po_marker, $logging) {
|
||||||
|
$poMgmt = new PurchaseOrderManager($pdo, $company_id);
|
||||||
|
$pid = $poMgmt->savePo([
|
||||||
|
'id' => 0, 'contact_id' => $supplier_id, 'department_id' => $wh_dept_id,
|
||||||
|
'po_date' => $today, 'expected_date' => date('Y-m-d', strtotime('+7 days')),
|
||||||
|
'warehouse_id' => $main_wh, 'items' => $po_items, 'discount' => 0, 'shipping_fee' => 0,
|
||||||
|
'notes' => $po_marker,
|
||||||
|
], $logging);
|
||||||
|
$poMgmt->confirmPo($pid, $logging);
|
||||||
|
return $pid;
|
||||||
|
});
|
||||||
|
ok("Created + confirmed PO for 3 laptops and 1 ERP licence (id={$po_id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pi_id = $lookup(
|
||||||
|
"SELECT id FROM td_invoice WHERE company_id = :c AND source = 'po' AND source_id = :p AND doc_type = 'purchase_invoice' AND status <> 4 LIMIT 1",
|
||||||
|
[':c' => $company_id, ':p' => $po_id]
|
||||||
|
);
|
||||||
|
if ($pi_id) {
|
||||||
|
skip("Purchase invoice for the asset PO already exists (id={$pi_id})");
|
||||||
|
} else {
|
||||||
|
$pi_id = dbTransaction($pdo2, function ($pdo) use ($company_id, $po_id, $logging) {
|
||||||
|
$invMgmt = new InvoiceManager($pdo, $company_id);
|
||||||
|
$iid = $invMgmt->createFromPo($po_id, $logging);
|
||||||
|
$invMgmt->issueInvoice($iid, $logging, date('Y-m-d', strtotime('+30 days')));
|
||||||
|
return $iid;
|
||||||
|
});
|
||||||
|
ok("Created + issued purchase invoice from the asset PO (id={$pi_id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
$gl = new GlManager($pdo2, $company_id, new PostingWindowGuard($pdo1, $company_id));
|
||||||
|
if ($gl->getBySource('purchase_invoice', $pi_id)) {
|
||||||
|
skip("GL already posted for purchase invoice #{$pi_id}");
|
||||||
|
} else {
|
||||||
|
// Same as order/api/engine/issue_invoice.php's auto-post.
|
||||||
|
$built = (new PurchaseInvoicePosting($pdo2, $company_id))->build($pi_id, null);
|
||||||
|
$pdo2->beginTransaction();
|
||||||
|
$gl->post('purchase_invoice', $pi_id, $built['formula_id'], $built['period'], $built['lines'], ['journal_date' => $built['doc_date']]);
|
||||||
|
$pdo2->commit();
|
||||||
|
ok("GL posted for purchase invoice #{$pi_id} (" . count($built['lines']) . " lines)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 3. Assets
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo "\n--- Assets ---\n";
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare("SELECT product_sku, id FROM td_invoice_item WHERE company_id = :c AND invoice_id = :i");
|
||||||
|
$sth->execute([':c' => $company_id, ':i' => $pi_id]);
|
||||||
|
$invoice_items = $sth->fetchAll(PDO::FETCH_KEY_PAIR); // sku => td_invoice_item.id
|
||||||
|
|
||||||
|
$asset_defs = [
|
||||||
|
['key' => 'laptops', 'name' => 'Laptop fleet - Dell Latitude 5440 (3 units)', 'category' => 'IT', 'sku' => 'FA-LAPTOP', 'salvage' => 6000, 'life' => 36],
|
||||||
|
['key' => 'erp', 'name' => 'ERP software licence', 'category' => 'SW', 'sku' => 'FA-ERP', 'salvage' => 0, 'life' => 24],
|
||||||
|
['key' => 'truck', 'name' => 'Delivery truck - Isuzu D-Max', 'category' => 'VEH', 'acquired' => date('Y-m-15', strtotime('first day of -18 months')), 'cost' => 850000, 'salvage' => 150000, 'life' => 60],
|
||||||
|
['key' => 'forklift','name' => 'Forklift - Toyota 8FD25', 'category' => 'VEH', 'acquired' => date('Y-m-01', strtotime('first day of -30 months')), 'cost' => 420000, 'salvage' => 20000, 'life' => 48],
|
||||||
|
];
|
||||||
|
|
||||||
|
$asset_ids = [];
|
||||||
|
foreach ($asset_defs as $d) {
|
||||||
|
$sth = $pdo2->prepare("SELECT id, status FROM td_asset WHERE company_id = :c AND asset_name = :n AND status <> 4 LIMIT 1");
|
||||||
|
$sth->execute([':c' => $company_id, ':n' => $d['name']]);
|
||||||
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
skip("Asset '{$d['name']}' already exists (id={$row['id']})");
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
} else {
|
||||||
|
$data = [
|
||||||
|
'asset_name' => $d['name'], 'category_id' => $category_ids[$d['category']],
|
||||||
|
'salvage_value' => $d['salvage'], 'useful_life_months' => $d['life'],
|
||||||
|
'department_id' => $wh_dept_id, 'description' => 'Demo seed',
|
||||||
|
];
|
||||||
|
if (!empty($d['sku'])) {
|
||||||
|
// Capitalized from the purchase invoice line, as asset/manage_asset.php?invoice_id=&item_id= does.
|
||||||
|
$item_id = (int)($invoice_items[$d['sku']] ?? 0);
|
||||||
|
$line = (new AssetManager($pdo2, $company_id))->getInvoiceLine($pi_id, $item_id);
|
||||||
|
$data += [
|
||||||
|
'invoice_id' => $pi_id, 'invoice_item_id' => $item_id,
|
||||||
|
'cost' => $line['remaining'], 'clearing_account_code' => $line['clearing_account_code'],
|
||||||
|
'acquisition_date' => $line['issued_date'], 'in_service_date' => $line['issued_date'],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Bought before the system was used: already on the books, so no clearing account.
|
||||||
|
$data += ['cost' => $d['cost'], 'clearing_account_code' => '', 'acquisition_date' => $d['acquired'], 'in_service_date' => $d['acquired']];
|
||||||
|
}
|
||||||
|
$id = dbTransaction($pdo2, fn($pdo) => (new AssetManager($pdo, $company_id))->save($data, $owner_user_id));
|
||||||
|
ok("Created asset '{$d['name']}' (id={$id}), cost " . number_format((float)$data['cost'], 2) . ", in service {$data['in_service_date']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)($row['status'] ?? 0) === 0) {
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new AssetManager($pdo, $company_id))->activate($id));
|
||||||
|
ok("Activated '{$d['name']}'");
|
||||||
|
}
|
||||||
|
$asset_ids[$d['key']] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 4. Runs and the forklift sale
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo "\n--- Depreciation / amortization runs ---\n";
|
||||||
|
|
||||||
|
$run_through = function (string $asset_class, string $period) use ($pdo2, $company_id, $owner_user_id) {
|
||||||
|
$label = AssetRunManager::labelFor($asset_class);
|
||||||
|
if (!(new AssetRunManager($pdo2, $company_id))->preview($asset_class, $period)) {
|
||||||
|
skip("{$label} already charged through {$period}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$created = dbTransaction($pdo2, fn($pdo) => (new AssetRunManager($pdo, $company_id))->run($asset_class, $period, $owner_user_id));
|
||||||
|
$first = $created[0]['run_number'];
|
||||||
|
$last = $created[count($created) - 1]['run_number'];
|
||||||
|
ok("{$label}: " . count($created) . " run(s) {$first}" . ($first !== $last ? " .. {$last}" : '')
|
||||||
|
. ", total " . number_format(array_sum(array_column($created, 'total')), 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
$run_through('tangible', $last_period);
|
||||||
|
|
||||||
|
$forklift = (new AssetManager($pdo2, $company_id))->getById($asset_ids['forklift']);
|
||||||
|
if (in_array((int)$forklift['status'], [AssetManager::STATUS_ACTIVE, AssetManager::STATUS_FULLY], true)) {
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new AssetManager($pdo, $company_id))->dispose($asset_ids['forklift'], [
|
||||||
|
'disposal_date' => $today, 'sale_price' => 180000, 'proceeds_account_code' => '1000',
|
||||||
|
'disposal_notes' => 'Sold to a used-equipment dealer (demo seed)',
|
||||||
|
]));
|
||||||
|
ok("Sold '{$forklift['asset_name']}' today for 180,000.00 (book value " . number_format((float)$forklift['book_value'], 2) . ")");
|
||||||
|
} else {
|
||||||
|
skip("'{$forklift['asset_name']}' already disposed of");
|
||||||
|
}
|
||||||
|
|
||||||
|
$run_through('tangible', $this_period);
|
||||||
|
$run_through('intangible', $this_period);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 5. GL posting (Batch GL Entries equivalent)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
echo "\n--- GL posting ---\n";
|
||||||
|
|
||||||
|
$post_asset_gl = function (string $source_type, int $id) use ($pdo1, $pdo2, $company_id) {
|
||||||
|
$gl = new GlManager($pdo2, $company_id, new PostingWindowGuard($pdo1, $company_id));
|
||||||
|
if ($gl->getBySource($source_type, $id)) return null;
|
||||||
|
|
||||||
|
$built = (new AssetPosting($pdo2, $company_id))->build($source_type, $id);
|
||||||
|
$pdo2->beginTransaction();
|
||||||
|
try {
|
||||||
|
$gl->post($source_type, $id, 0, $built['period'], $built['lines'], [
|
||||||
|
'journal_date' => $built['doc_date'], 'reference' => $built['reference'], 'description' => $built['description'],
|
||||||
|
]);
|
||||||
|
$pdo2->commit();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo2->inTransaction()) $pdo2->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return $built;
|
||||||
|
};
|
||||||
|
|
||||||
|
$queue = [];
|
||||||
|
$sth = $pdo2->prepare("SELECT id FROM td_asset WHERE company_id = :c AND status IN (1, 2, 3) AND clearing_account_code <> '' ORDER BY id");
|
||||||
|
$sth->execute([':c' => $company_id]);
|
||||||
|
foreach ($sth->fetchAll(PDO::FETCH_COLUMN) as $id) $queue[] = ['asset_capitalization', (int)$id];
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare("SELECT id, asset_class FROM td_asset_run WHERE company_id = :c AND status = 1 AND period < :p ORDER BY period, id");
|
||||||
|
$sth->execute([':c' => $company_id, ':p' => $this_period]);
|
||||||
|
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $r) $queue[] = [AssetRunManager::sourceTypeFor($r['asset_class']), (int)$r['id']];
|
||||||
|
|
||||||
|
$sth = $pdo2->prepare("SELECT id FROM td_asset WHERE company_id = :c AND status = 3 ORDER BY id");
|
||||||
|
$sth->execute([':c' => $company_id]);
|
||||||
|
foreach ($sth->fetchAll(PDO::FETCH_COLUMN) as $id) $queue[] = ['asset_disposal', (int)$id];
|
||||||
|
|
||||||
|
$posted = 0;
|
||||||
|
foreach ($queue as [$source_type, $id]) {
|
||||||
|
$built = $post_asset_gl($source_type, $id);
|
||||||
|
if ($built) {
|
||||||
|
$posted++;
|
||||||
|
ok("Posted {$built['reference']} — {$built['description']}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$posted) skip("Asset GL entries already posted");
|
||||||
|
info("This month's runs are left unposted — post them from Accounting > Batch GL Entries.");
|
||||||
|
|
||||||
|
echo "\n\033[1m=== Done ===\033[0m\n\n";
|
||||||
@@ -39,7 +39,7 @@ function buildLineItem(array $products, string $sku, float $qty): array {
|
|||||||
$tax_amount = round($total_price * $tax_rate / 100, 4);
|
$tax_amount = round($total_price * $tax_rate / 100, 4);
|
||||||
return [
|
return [
|
||||||
'product_sku' => $sku,
|
'product_sku' => $sku,
|
||||||
'product_name' => $sku,
|
'product_name' => $p['product_name'],
|
||||||
'quantity' => $qty,
|
'quantity' => $qty,
|
||||||
'unit_price' => $unit_price,
|
'unit_price' => $unit_price,
|
||||||
'total_price' => $total_price,
|
'total_price' => $total_price,
|
||||||
@@ -69,11 +69,16 @@ $sth->execute([':c' => $company_id]);
|
|||||||
$sales_dept_id = (int)$sth->fetchColumn();
|
$sales_dept_id = (int)$sth->fetchColumn();
|
||||||
if (!$sales_dept_id) { exit("ERROR: SALES department not found — run demo_seed_transactions.php first.\n"); }
|
if (!$sales_dept_id) { exit("ERROR: SALES department not found — run demo_seed_transactions.php first.\n"); }
|
||||||
|
|
||||||
$sth = $pdo2->prepare("SELECT sku, price FROM md_product WHERE company_id = :c");
|
$sth = $pdo2->prepare("SELECT sku, product_name, price FROM md_product WHERE company_id = :c");
|
||||||
$sth->execute([':c' => $company_id]);
|
$sth->execute([':c' => $company_id]);
|
||||||
$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
||||||
|
|
||||||
$sth = $pdo2->prepare("SELECT id, contact_name FROM md_contact WHERE company_id = :c AND contact_type = 1 ORDER BY id");
|
// contact_type holds an md_contact_type id — resolve 'Customer' by name, not by a fixed id.
|
||||||
|
$sth = $pdo2->prepare(
|
||||||
|
"SELECT c.id, c.contact_name FROM md_contact c
|
||||||
|
JOIN md_contact_type t ON t.company_id = c.company_id AND t.id = c.contact_type
|
||||||
|
WHERE c.company_id = :c AND t.contact_type = 'Customer' ORDER BY c.id"
|
||||||
|
);
|
||||||
$sth->execute([':c' => $company_id]);
|
$sth->execute([':c' => $company_id]);
|
||||||
$customers = $sth->fetchAll(PDO::FETCH_ASSOC);
|
$customers = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$customer_ids = array_column($customers, 'id');
|
$customer_ids = array_column($customers, 'id');
|
||||||
|
|||||||
+46
-173
@@ -5,18 +5,16 @@
|
|||||||
*
|
*
|
||||||
* Extends the base demo data (demo_seed.php: company, warehouses, products,
|
* Extends the base demo data (demo_seed.php: company, warehouses, products,
|
||||||
* contacts, opening stock) with transactional data covering the rest of the
|
* contacts, opening stock) with transactional data covering the rest of the
|
||||||
* app's features:
|
* app's features (restock / dispatch / transfer movements are seeded by demo_seed.php):
|
||||||
*
|
*
|
||||||
* 1. Additional stock-in replenishment (restock events)
|
* 4. Chart of accounts, departments, GL posting formulas, product account mapping
|
||||||
* 2. Stock-out (direct dispatch) + stock transfer (Main -> Bangna)
|
* 5. Sales cycle: quotation -> sales order -> invoice -> GL post
|
||||||
* 3. Chart of accounts, departments, GL posting formulas
|
* 6. AR: receipt billing -> receipt -> GL post
|
||||||
* 4. Sales cycle: quotation -> sales order -> invoice -> GL post
|
* 7. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post
|
||||||
* 5. AR: receipt billing -> receipt -> GL post
|
* 8. AP: payment billing -> payment -> GL post
|
||||||
* 6. Purchasing cycle: purchase request -> PO -> receive -> purchase invoice -> GL post
|
* 9. Supplier return (confirmed, restocks reversed)
|
||||||
* 7. AP: payment billing -> payment -> GL post
|
* 10. Customer return (draft only — see note below)
|
||||||
* 8. Supplier return (confirmed, restocks reversed)
|
* 11. Barcode labels for a handful of products
|
||||||
* 9. Customer return (draft only — see note below)
|
|
||||||
* 10. Barcode labels for a handful of products
|
|
||||||
*
|
*
|
||||||
* Every write goes through the same Manager classes + engine-file patterns
|
* Every write goes through the same Manager classes + engine-file patterns
|
||||||
* the app itself uses (dbTransaction wrapping, GlManager posting exactly as
|
* the app itself uses (dbTransaction wrapping, GlManager posting exactly as
|
||||||
@@ -46,6 +44,7 @@ require_once __DIR__ . '/app/dbconn.php';
|
|||||||
require_once __DIR__ . '/app/assets/utils/db_helpers.php';
|
require_once __DIR__ . '/app/assets/utils/db_helpers.php';
|
||||||
require_once __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
|
require_once __DIR__ . '/app/assets/utils/classes/WarehouseManager.php';
|
||||||
require_once __DIR__ . '/app/assets/utils/classes/StockManager.php';
|
require_once __DIR__ . '/app/assets/utils/classes/StockManager.php';
|
||||||
|
require_once __DIR__ . '/app/assets/utils/classes/ProductManager.php';
|
||||||
require_once __DIR__ . '/app/assets/utils/classes/QuotationManager.php';
|
require_once __DIR__ . '/app/assets/utils/classes/QuotationManager.php';
|
||||||
require_once __DIR__ . '/app/assets/utils/classes/OrderManager.php';
|
require_once __DIR__ . '/app/assets/utils/classes/OrderManager.php';
|
||||||
require_once __DIR__ . '/app/assets/utils/classes/InvoiceManager.php';
|
require_once __DIR__ . '/app/assets/utils/classes/InvoiceManager.php';
|
||||||
@@ -89,24 +88,29 @@ $sth->execute();
|
|||||||
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
|
$owner_user_id = (int)($sth->fetchColumn() ?: 0);
|
||||||
if (!$owner_user_id) exit("ERROR: demo owner user not found — run demo_seed.php first.\n");
|
if (!$owner_user_id) exit("ERROR: demo owner user not found — run demo_seed.php first.\n");
|
||||||
|
|
||||||
|
// Resolve by name: ids depend on what else exists in the database.
|
||||||
$sth = $pdo2->prepare("SELECT id, warehouse_name FROM md_warehouse WHERE company_id = :c ORDER BY id");
|
$sth = $pdo2->prepare("SELECT id, warehouse_name FROM md_warehouse WHERE company_id = :c ORDER BY id");
|
||||||
$sth->execute([':c' => $company_id]);
|
$sth->execute([':c' => $company_id]);
|
||||||
$warehouses = $sth->fetchAll(PDO::FETCH_KEY_PAIR); // id => name
|
$warehouses = $sth->fetchAll(PDO::FETCH_KEY_PAIR); // id => name
|
||||||
if (count($warehouses) < 2) exit("ERROR: expected 2 warehouses — run demo_seed.php first.\n");
|
$main_wh = (int)(array_search('Main Warehouse', $warehouses, true) ?: 0);
|
||||||
$wh_ids = array_keys($warehouses);
|
$bangna_wh = (int)(array_search('Bangna Distribution Center', $warehouses, true) ?: 0);
|
||||||
$main_wh = $wh_ids[0]; // Main Warehouse
|
if (!$main_wh || !$bangna_wh) exit("ERROR: expected Main Warehouse and Bangna Distribution Center — run demo_seed.php first.\n");
|
||||||
$bangna_wh = $wh_ids[1]; // Bangna Distribution Center
|
|
||||||
|
|
||||||
$sth = $pdo2->prepare("SELECT id, contact_name, contact_type FROM md_contact WHERE company_id = :c ORDER BY id");
|
// md_contact.contact_type is an md_contact_type id, so match on the type name.
|
||||||
|
$sth = $pdo2->prepare(
|
||||||
|
"SELECT c.id, c.contact_name, t.contact_type AS type_name
|
||||||
|
FROM md_contact c
|
||||||
|
JOIN md_contact_type t ON t.company_id = c.company_id AND t.id = c.contact_type
|
||||||
|
WHERE c.company_id = :c
|
||||||
|
ORDER BY c.id"
|
||||||
|
);
|
||||||
$sth->execute([':c' => $company_id]);
|
$sth->execute([':c' => $company_id]);
|
||||||
$contacts = $sth->fetchAll(PDO::FETCH_ASSOC);
|
$contacts = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
if (count($contacts) < 7) exit("ERROR: expected 7 contacts — run demo_seed.php first.\n");
|
$customer_ids = array_values(array_column(array_filter($contacts, fn($c) => $c['type_name'] === 'Customer'), 'id'));
|
||||||
$customer_ids = array_column(array_filter($contacts, fn($c) => (int)$c['contact_type'] === 1), 'id');
|
$supplier_ids = array_values(array_column(array_filter($contacts, fn($c) => $c['type_name'] === 'Supplier'), 'id'));
|
||||||
$supplier_ids = array_column(array_filter($contacts, fn($c) => (int)$c['contact_type'] === 2), 'id');
|
if (count($customer_ids) < 4 || count($supplier_ids) < 3) exit("ERROR: expected 4 customers and 3 suppliers — run demo_seed.php first.\n");
|
||||||
$customer_ids = array_values($customer_ids);
|
|
||||||
$supplier_ids = array_values($supplier_ids);
|
|
||||||
|
|
||||||
$sth = $pdo2->prepare("SELECT sku, uom, cost_price, price FROM md_product WHERE company_id = :c ORDER BY id");
|
$sth = $pdo2->prepare("SELECT sku, product_name, uom, cost_price, price FROM md_product WHERE company_id = :c ORDER BY id");
|
||||||
$sth->execute([':c' => $company_id]);
|
$sth->execute([':c' => $company_id]);
|
||||||
$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
$products = $sth->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
||||||
if (count($products) < 14) exit("ERROR: expected 14 products — run demo_seed.php first.\n");
|
if (count($products) < 14) exit("ERROR: expected 14 products — run demo_seed.php first.\n");
|
||||||
@@ -114,156 +118,6 @@ if (count($products) < 14) exit("ERROR: expected 14 products — run demo_seed.p
|
|||||||
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_transactions'];
|
$logging = ['user_id' => $owner_user_id, 'dt' => date('Y-m-d H:i:s'), 'login' => null, 'action' => 'seed_transactions'];
|
||||||
|
|
||||||
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
$whMgmt = new WarehouseManager($pdo2, $company_id);
|
||||||
$stockMgmt = new StockManager($pdo2, $company_id);
|
|
||||||
|
|
||||||
/** Find the next unused simple-location bin label "A-N" for a warehouse. */
|
|
||||||
function nextFreeBin(PDO $pdo2, int $company_id, int $warehouse_id): string {
|
|
||||||
$sth = $pdo2->prepare(
|
|
||||||
"SELECT bin FROM md_bin WHERE company_id = :c AND warehouse = :w AND product_sku IS NULL
|
|
||||||
ORDER BY CAST(SUBSTRING(bin, 3) AS UNSIGNED) ASC LIMIT 1"
|
|
||||||
);
|
|
||||||
$sth->execute([':c' => $company_id, ':w' => $warehouse_id]);
|
|
||||||
$bin = $sth->fetchColumn();
|
|
||||||
if (!$bin) throw new Exception("No free bin available in warehouse {$warehouse_id}.");
|
|
||||||
return $bin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// 1. Additional stock-in replenishment (restock events)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
echo "--- Restock (additional stock-in) ---\n";
|
|
||||||
|
|
||||||
$restock_defs = [
|
|
||||||
['sku' => 'EL-001', 'warehouse' => $main_wh, 'qty' => 40, 'supplier_idx' => 0],
|
|
||||||
['sku' => 'OF-001', 'warehouse' => $main_wh, 'qty' => 60, 'supplier_idx' => 1],
|
|
||||||
['sku' => 'BV-002', 'warehouse' => $bangna_wh, 'qty' => 35, 'supplier_idx' => 2],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($restock_defs as $r) {
|
|
||||||
$sth = $pdo2->prepare(
|
|
||||||
"SELECT COUNT(*) FROM `td_stock_{$r['warehouse']}`
|
|
||||||
WHERE company_id = :c AND product_sku = :sku AND type = 'in' AND description = 'Restock (demo seed)'"
|
|
||||||
);
|
|
||||||
$sth->execute([':c' => $company_id, ':sku' => $r['sku']]);
|
|
||||||
if ((int)$sth->fetchColumn() > 0) {
|
|
||||||
skip("Restock for {$r['sku']} in warehouse {$r['warehouse']} already exists");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bin = nextFreeBin($pdo2, $company_id, $r['warehouse']);
|
|
||||||
$supplier_id = $supplier_ids[$r['supplier_idx']];
|
|
||||||
$uuid = bin2hex(random_bytes(16));
|
|
||||||
$p = $products[$r['sku']];
|
|
||||||
|
|
||||||
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $r, $bin, $supplier_id, $logging, $uuid, $p) {
|
|
||||||
$stock_id = $stockMgmt->saveStockIn([
|
|
||||||
'id' => 0, 'warehouse' => $r['warehouse'], 'product_sku' => $r['sku'],
|
|
||||||
'quantity' => $r['qty'], 'price' => $p['cost_price'],
|
|
||||||
'zone' => $bin, 'aisle' => $bin, 'bin' => $bin,
|
|
||||||
'contact_id' => $supplier_id, 'description' => 'Restock (demo seed)',
|
|
||||||
], $logging, $uuid);
|
|
||||||
$stockMgmt->approveStock($stock_id, $r['warehouse'], 'in', $whMgmt);
|
|
||||||
});
|
|
||||||
ok("Restocked {$r['qty']} {$p['uom']} of {$r['sku']} into {$warehouses[$r['warehouse']]} bin {$bin}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// 2. Stock-out (direct dispatch) — dedicated batch in + full-bin out
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
echo "\n--- Stock-out (direct dispatch) ---\n";
|
|
||||||
|
|
||||||
$dispatch_defs = [
|
|
||||||
['sku' => 'PK-003', 'warehouse' => $main_wh, 'qty' => 25, 'customer_idx' => 0],
|
|
||||||
['sku' => 'OF-003', 'warehouse' => $main_wh, 'qty' => 15, 'customer_idx' => 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($dispatch_defs as $d) {
|
|
||||||
$sth = $pdo2->prepare(
|
|
||||||
"SELECT COUNT(*) FROM `td_stock_{$d['warehouse']}`
|
|
||||||
WHERE company_id = :c AND product_sku = :sku AND type = 'out' AND description = 'Direct dispatch (demo seed)'"
|
|
||||||
);
|
|
||||||
$sth->execute([':c' => $company_id, ':sku' => $d['sku']]);
|
|
||||||
if ((int)$sth->fetchColumn() > 0) {
|
|
||||||
skip("Dispatch for {$d['sku']} already exists");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bin = nextFreeBin($pdo2, $company_id, $d['warehouse']);
|
|
||||||
$customer_id = $customer_ids[$d['customer_idx']];
|
|
||||||
$p = $products[$d['sku']];
|
|
||||||
|
|
||||||
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $d, $bin, $customer_id, $logging, $p) {
|
|
||||||
// Receive a dedicated batch first (so we don't touch demo_seed.php's opening-stock bins)
|
|
||||||
$in_uuid = bin2hex(random_bytes(16));
|
|
||||||
$in_id = $stockMgmt->saveStockIn([
|
|
||||||
'id' => 0, 'warehouse' => $d['warehouse'], 'product_sku' => $d['sku'],
|
|
||||||
'quantity' => $d['qty'], 'price' => $p['cost_price'],
|
|
||||||
'zone' => $bin, 'aisle' => $bin, 'bin' => $bin,
|
|
||||||
'contact_id' => $customer_id, 'description' => 'Batch for dispatch (demo seed)',
|
|
||||||
], $logging, $in_uuid);
|
|
||||||
$stockMgmt->approveStock($in_id, $d['warehouse'], 'in', $whMgmt);
|
|
||||||
|
|
||||||
// Dispatch it straight out (saveStockOut takes the whole bin)
|
|
||||||
$out_uuid = bin2hex(random_bytes(16));
|
|
||||||
$out_id = $stockMgmt->saveStockOut([
|
|
||||||
'id' => 0, 'warehouse' => $d['warehouse'], 'product_sku' => $d['sku'],
|
|
||||||
'zone' => $bin, 'aisle' => $bin, 'bin' => $bin,
|
|
||||||
'contact_id' => $customer_id, 'description' => 'Direct dispatch (demo seed)',
|
|
||||||
], $logging, $out_uuid);
|
|
||||||
$stockMgmt->approveStock($out_id, $d['warehouse'], 'out', $whMgmt);
|
|
||||||
});
|
|
||||||
ok("Dispatched {$d['qty']} {$p['uom']} of {$d['sku']} from {$warehouses[$d['warehouse']]}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// 3. Stock transfer — Main Warehouse -> Bangna Distribution Center
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
echo "\n--- Stock transfer (Main -> Bangna) ---\n";
|
|
||||||
|
|
||||||
$transfer_defs = [
|
|
||||||
['sku' => 'BV-001', 'qty' => 30],
|
|
||||||
['sku' => 'PK-002', 'qty' => 12],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($transfer_defs as $t) {
|
|
||||||
$sth = $pdo2->prepare(
|
|
||||||
"SELECT COUNT(*) FROM `td_stock_{$main_wh}`
|
|
||||||
WHERE company_id = :c AND product_sku = :sku AND type = 'transfer' AND description = 'Transfer to Bangna (demo seed)'"
|
|
||||||
);
|
|
||||||
$sth->execute([':c' => $company_id, ':sku' => $t['sku']]);
|
|
||||||
if ((int)$sth->fetchColumn() > 0) {
|
|
||||||
skip("Transfer for {$t['sku']} already exists");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bin allocation is independent of the transaction below — resolve both up front.
|
|
||||||
$from_bin = nextFreeBin($pdo2, $company_id, $main_wh);
|
|
||||||
$to_bin = nextFreeBin($pdo2, $company_id, $bangna_wh);
|
|
||||||
$p = $products[$t['sku']];
|
|
||||||
|
|
||||||
dbTransaction($pdo2, function ($pdo2) use ($stockMgmt, $whMgmt, $t, $from_bin, $to_bin, $main_wh, $bangna_wh, $logging, $p) {
|
|
||||||
// Receive a dedicated batch first (so we don't touch demo_seed.php's opening-stock bins)
|
|
||||||
$in_uuid = bin2hex(random_bytes(16));
|
|
||||||
$in_id = $stockMgmt->saveStockIn([
|
|
||||||
'id' => 0, 'warehouse' => $main_wh, 'product_sku' => $t['sku'],
|
|
||||||
'quantity' => $t['qty'], 'price' => $p['cost_price'],
|
|
||||||
'zone' => $from_bin, 'aisle' => $from_bin, 'bin' => $from_bin,
|
|
||||||
'contact_id' => 0, 'description' => 'Batch for transfer (demo seed)',
|
|
||||||
], $logging, $in_uuid);
|
|
||||||
$stockMgmt->approveStock($in_id, $main_wh, 'in', $whMgmt);
|
|
||||||
|
|
||||||
$tr_uuid = bin2hex(random_bytes(16));
|
|
||||||
$out_id = $stockMgmt->saveStockTransfer([
|
|
||||||
'id' => 0, 'warehouse_from' => $main_wh, 'warehouse_to' => $bangna_wh,
|
|
||||||
'product_sku' => $t['sku'],
|
|
||||||
'zone_from' => $from_bin, 'aisle_from' => $from_bin, 'bin_from' => $from_bin,
|
|
||||||
'zone_to' => $to_bin, 'aisle_to' => $to_bin, 'bin_to' => $to_bin,
|
|
||||||
'contact_id' => 0, 'description' => 'Transfer to Bangna (demo seed)',
|
|
||||||
], $logging, $tr_uuid);
|
|
||||||
$stockMgmt->approveStock($out_id, $main_wh, 'transfer', $whMgmt);
|
|
||||||
});
|
|
||||||
ok("Transferred {$t['qty']} {$p['uom']} of {$t['sku']}: {$warehouses[$main_wh]} bin {$from_bin} -> {$warehouses[$bangna_wh]} bin {$to_bin}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 4. Chart of accounts + departments + GL posting formulas
|
// 4. Chart of accounts + departments + GL posting formulas
|
||||||
@@ -379,6 +233,25 @@ foreach ($formula_defs as $doc_type => $def) {
|
|||||||
$formula_ids[$doc_type] = $id;
|
$formula_ids[$doc_type] = $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo "\n--- Product account mapping ---\n";
|
||||||
|
|
||||||
|
// The sales and purchase formulas split 'total' per product, so Batch GL Entries
|
||||||
|
// refuses to post until every product has sales/purchase accounts
|
||||||
|
// (Account Formulas > Product Accounts, accounting/api/engine/product_account_mapping.php).
|
||||||
|
$sth = $pdo2->prepare("SELECT id, sku, sales_account_code, purchase_account_code FROM md_product WHERE company_id = :c ORDER BY id");
|
||||||
|
$sth->execute([':c' => $company_id]);
|
||||||
|
foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $prod) {
|
||||||
|
if ($prod['sales_account_code'] && $prod['purchase_account_code']) {
|
||||||
|
skip("Product {$prod['sku']} already mapped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sales_code = $prod['sales_account_code'] ?: '4000';
|
||||||
|
$purchase_code = $prod['purchase_account_code'] ?: '5000';
|
||||||
|
dbTransaction($pdo2, fn($pdo) => (new ProductManager($pdo, $company_id))
|
||||||
|
->updateAccountMapping((int)$prod['id'], $sales_code, $purchase_code, $logging));
|
||||||
|
ok("Mapped {$prod['sku']}: sales {$sales_code}, purchase {$purchase_code}");
|
||||||
|
}
|
||||||
|
|
||||||
/** Post (or replace) GL for a document, mirroring order/api/engine/issue_invoice.php exactly. */
|
/** Post (or replace) GL for a document, mirroring order/api/engine/issue_invoice.php exactly. */
|
||||||
function postGl(PDO $pdo1, PDO $pdo2, int $company_id, string $doc_type, int $doc_id, string $posting_class): array {
|
function postGl(PDO $pdo1, PDO $pdo2, int $company_id, string $doc_type, int $doc_id, string $posting_class): array {
|
||||||
require_once __DIR__ . "/app/assets/utils/classes_ac/posting/{$posting_class}.php";
|
require_once __DIR__ . "/app/assets/utils/classes_ac/posting/{$posting_class}.php";
|
||||||
@@ -409,7 +282,7 @@ function buildLineItem(array $products, string $sku, float $qty): array {
|
|||||||
$tax_amount = round($total_price * $tax_rate / 100, 4);
|
$tax_amount = round($total_price * $tax_rate / 100, 4);
|
||||||
return [
|
return [
|
||||||
'product_sku' => $sku,
|
'product_sku' => $sku,
|
||||||
'product_name' => $sku, // demo_seed.php products keyed by SKU; name not needed for GL/report correctness
|
'product_name' => $p['product_name'], // documents show the product name, as the UI's product search fills it
|
||||||
'quantity' => $qty,
|
'quantity' => $qty,
|
||||||
'unit_price' => $unit_price,
|
'unit_price' => $unit_price,
|
||||||
'total_price' => $total_price,
|
'total_price' => $total_price,
|
||||||
|
|||||||
Reference in New Issue
Block a user