Add Asset Management app
Assets from purchase invoice lines, straight-line depreciation/amortization with salvage value, disposals, and [Asset Mgmt] sources in Batch GL Entries. Green third workspace with an app access guard.
This commit is contained in:
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
|
||||
$asset_id = (int)($_GET['id'] ?? 0);
|
||||
$invoice_id = (int)($_GET['invoice_id'] ?? 0);
|
||||
$item_id = (int)($_GET['item_id'] ?? 0);
|
||||
$pi_link_ok = in_array($_SESSION['login_app_access'] ?? 'wms', ['all', 'accounting'], true);
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php'; ?>
|
||||
<?php require '../include_sidebar_asset.php'; ?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-6 d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1" id="page_title">New Asset</h1>
|
||||
<p class="mb-0 text-muted" id="page_subtitle">Straight-line depreciation with your own salvage value</p>
|
||||
</div>
|
||||
<a href="<?php echo $server_url?>asset/index.php" class="btn btn-light">
|
||||
<i class="ti ti-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
|
||||
<!-- Left: form + schedule -->
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="alert alert-info small d-none" id="source_alert"></div>
|
||||
|
||||
<div class="card mb-5" id="assetForm">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-5 mb-4">Asset Details</h2>
|
||||
<input type="hidden" id="id" value="<?php echo $asset_id ?: ''; ?>">
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Asset Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="asset_name" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Department</label>
|
||||
<select class="form-select" id="department_id"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="category_id" required></select>
|
||||
<div class="form-text" id="class_hint"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Clearing Account</label>
|
||||
<select class="form-select" id="clearing_account_code"></select>
|
||||
<div class="form-text">Credited when the asset is capitalized — the account the purchase was posted to. Leave empty if the asset is already on the books.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Acquisition Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="acquisition_date" placeholder="DD/MM/YYYY" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">In-Service Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="in_service_date" placeholder="DD/MM/YYYY" required>
|
||||
</div>
|
||||
<div class="col-md-6"></div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Cost <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control text-end" id="cost" min="0" step="0.01" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Salvage Value</label>
|
||||
<input type="number" class="form-control text-end" id="salvage_value" min="0" step="0.01" value="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Useful Life (months) <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control text-end" id="useful_life_months" min="1" max="1200" step="1" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="bg-light rounded-2 px-4 py-3 d-flex flex-wrap gap-4 small">
|
||||
<div><span class="text-muted">Amount to <span class="label-charge">depreciate</span>:</span> <strong id="calc_base">—</strong></div>
|
||||
<div><span class="text-muted">Per month:</span> <strong id="calc_monthly">—</strong></div>
|
||||
<div><span class="text-muted">Last month:</span> <strong id="calc_last">—</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" rows="2" placeholder="Serial number, location, notes"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-3 d-none" id="locked_hint">
|
||||
<i class="ti ti-lock me-1"></i>Cost, salvage value, life, dates and category are locked because months have already been charged.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card d-none" id="schedule_card">
|
||||
<div class="card-body p-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fs-5 mb-0"><span class="label-schedule">Depreciation</span> Schedule</h2>
|
||||
<span class="small text-muted" id="schedule_summary"></span>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height:480px;">
|
||||
<table class="table table-sm mb-0 table-centered">
|
||||
<thead class="table-light" style="position:sticky; top:0;">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Month</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Accumulated</th>
|
||||
<th class="text-end">Book Value</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedule_tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: status + actions -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<div class="card mb-5 d-none" id="status_card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="fs-5 mb-3">Status</h2>
|
||||
<div class="mb-3 d-flex gap-2 flex-wrap" id="status_badges"></div>
|
||||
<div class="small mb-2"><span class="text-muted">Accumulated:</span> <span class="fw-semibold ms-1" id="display_accumulated">—</span></div>
|
||||
<div class="small mb-2"><span class="text-muted">Book value:</span> <span class="fw-semibold ms-1" id="display_book_value">—</span></div>
|
||||
<div class="small mb-2"><span class="text-muted">Last month charged:</span> <span class="ms-1" id="display_last_period">—</span></div>
|
||||
<div class="small mb-2 d-none" id="source_row"><span class="text-muted">Source:</span> <span class="ms-1" id="display_source"></span></div>
|
||||
|
||||
<hr>
|
||||
<h3 class="fs-6 mb-2">GL Entries <span class="badge bg-success text-white ms-1">[Asset Mgmt]</span></h3>
|
||||
<div class="small mb-2"><div class="text-muted">Capitalization</div><div id="gl_capitalization"></div></div>
|
||||
<div class="small mb-2 d-none" id="gl_disposal_row"><div class="text-muted">Disposal</div><div id="gl_disposal"></div></div>
|
||||
|
||||
<div class="d-none" id="disposal_info">
|
||||
<hr>
|
||||
<h3 class="fs-6 mb-2">Disposal</h3>
|
||||
<div class="small mb-1"><span class="text-muted">Date:</span> <span class="ms-1" id="display_disposal_date"></span></div>
|
||||
<div class="small mb-1"><span class="text-muted">Sale price:</span> <span class="ms-1" id="display_sale_price"></span></div>
|
||||
<div class="small mb-1"><span class="text-muted">Gain / (loss):</span> <span class="ms-1 fw-semibold" id="display_gain_loss"></span></div>
|
||||
<div class="small mb-1 text-muted" id="display_disposal_notes"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex flex-column gap-2">
|
||||
<button class="btn btn-primary w-100" id="btn_save" onclick="save_asset()">
|
||||
<i class="ti ti-device-floppy me-1"></i><span>Save as Draft</span>
|
||||
</button>
|
||||
<button class="btn btn-success w-100 d-none" id="btn_activate" onclick="asset_action('activate')">
|
||||
<i class="ti ti-circle-check me-1"></i>Activate
|
||||
</button>
|
||||
<button class="btn btn-outline-dark w-100 d-none" id="btn_dispose" onclick="open_disposal()">
|
||||
<i class="ti ti-receipt-refund me-1"></i>Sell / Dispose
|
||||
</button>
|
||||
<button class="btn btn-outline-warning w-100 d-none" id="btn_undo_disposal" onclick="asset_action('undo_disposal')">
|
||||
<i class="ti ti-arrow-back-up me-1"></i>Undo Disposal
|
||||
</button>
|
||||
<button class="btn btn-outline-danger w-100 d-none" id="btn_void" onclick="asset_action('void')">
|
||||
<i class="ti ti-ban me-1"></i>Void
|
||||
</button>
|
||||
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="asset_action('delete')">
|
||||
<i class="ti ti-trash me-1"></i>Delete Draft
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Disposal modal -->
|
||||
<div class="modal fade" id="disposal_modal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Sell / Dispose of Asset</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Disposal Date</label>
|
||||
<input type="text" class="form-control" id="disposal_date" placeholder="DD/MM/YYYY">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Sale Price</label>
|
||||
<input type="number" class="form-control text-end" id="sale_price" min="0" step="0.01" value="0">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Proceeds Account</label>
|
||||
<select class="form-select" id="proceeds_account_code"></select>
|
||||
<div class="form-text">Cash, bank or receivable that received the money. Not needed for a write-off (price 0).</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="disposal_notes" rows="2" placeholder="Buyer, reason"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="bg-light rounded-2 px-3 py-2 small">
|
||||
<div class="d-flex justify-content-between"><span class="text-muted">Cost</span><span id="dp_cost"></span></div>
|
||||
<div class="d-flex justify-content-between"><span class="text-muted">Accumulated (months charged so far)</span><span id="dp_accumulated"></span></div>
|
||||
<div class="d-flex justify-content-between"><span class="text-muted">Book value</span><span id="dp_book"></span></div>
|
||||
<div class="d-flex justify-content-between fw-semibold"><span id="dp_result_label">Gain / (loss)</span><span id="dp_result"></span></div>
|
||||
</div>
|
||||
<div class="form-text">Every month before the disposal month must already be charged. The disposal month itself is not charged.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Back</button>
|
||||
<button class="btn btn-dark" onclick="submit_disposal()"><i class="ti ti-receipt-refund me-1"></i>Record Disposal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require '../include_ending.php'; ?>
|
||||
<script src="<?php echo $server_url?>assets/js/asset.js"></script>
|
||||
|
||||
<script>
|
||||
var asset_id = <?php echo $asset_id; ?>;
|
||||
var invoice_id = <?php echo $invoice_id; ?>;
|
||||
var item_id = <?php echo $item_id; ?>;
|
||||
var pi_link_ok = <?php echo $pi_link_ok ? 'true' : 'false'; ?>;
|
||||
var can_admin = ['owner', 'admin'].includes(user_role);
|
||||
var can_edit = ['owner', 'admin', 'staff'].includes(user_role);
|
||||
|
||||
var categories = [];
|
||||
var accounts = [];
|
||||
var detail = null;
|
||||
var invoice_line = null;
|
||||
var life_touched = false;
|
||||
|
||||
var FINANCIAL_FIELDS = '#category_id, #acquisition_date, #in_service_date, #cost, #salvage_value, #useful_life_months, #clearing_account_code';
|
||||
|
||||
// ── lookups ────────────────────────────────────────────────────────────────
|
||||
function load_categories() {
|
||||
return ajax_request({
|
||||
url: server_url + 'asset/api/engine/category.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
noLoading: true,
|
||||
onSuccess: function(res) { categories = res.output || []; }
|
||||
});
|
||||
}
|
||||
|
||||
function category_by_id(id) {
|
||||
return categories.find(function(c) { return String(c.id) === String(id); }) || null;
|
||||
}
|
||||
|
||||
function fill_category_select(selected) {
|
||||
var html = '<option value="">— Select category —</option>';
|
||||
categories.forEach(function(c) {
|
||||
if (String(c.status) !== '1' && String(c.id) !== String(selected)) return;
|
||||
html += '<option value="' + c.id + '">' + escape_html(c.category_code + ' — ' + c.category_name)
|
||||
+ (c.asset_class === 'intangible' ? ' (intangible)' : '') + '</option>';
|
||||
});
|
||||
$('#category_id').html(html).val(selected ? String(selected) : '');
|
||||
}
|
||||
|
||||
function update_class_hint() {
|
||||
var c = category_by_id($('#category_id').val());
|
||||
var intangible = c && c.asset_class === 'intangible';
|
||||
$('#class_hint').html(c ? asset_class_badge(c.asset_class) : '');
|
||||
$('.label-charge').text(intangible ? 'amortize' : 'depreciate');
|
||||
$('.label-schedule').text(intangible ? 'Amortization' : 'Depreciation');
|
||||
}
|
||||
|
||||
// ── live calculation ───────────────────────────────────────────────────────
|
||||
function update_calc() {
|
||||
var cost = parseFloat($('#cost').val()) || 0;
|
||||
var salvage = parseFloat($('#salvage_value').val()) || 0;
|
||||
var life = parseInt($('#useful_life_months').val(), 10) || 0;
|
||||
var base = round_dp(cost - salvage, 2);
|
||||
if (base <= 0 || life <= 0) {
|
||||
$('#calc_base, #calc_monthly, #calc_last').text('—');
|
||||
return;
|
||||
}
|
||||
var monthly = round_dp(base / life, 2);
|
||||
var last = round_dp(base - monthly * (life - 1), 2);
|
||||
$('#calc_base').text(format_number(base, 2));
|
||||
$('#calc_monthly').text(format_number(monthly, 2));
|
||||
$('#calc_last').text(life > 1 ? format_number(Math.max(last, 0), 2) : format_number(base, 2));
|
||||
}
|
||||
|
||||
// ── load ───────────────────────────────────────────────────────────────────
|
||||
function retrieve_asset() {
|
||||
return ajax_request({
|
||||
url: server_url + 'asset/api/engine/retrieve_asset.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { id: asset_id },
|
||||
onSuccess: function(res) {
|
||||
detail = res.output;
|
||||
render_detail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_invoice_line() {
|
||||
return ajax_request({
|
||||
url: server_url + 'asset/api/engine/invoice_lines.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'line',
|
||||
data: { invoice_id: invoice_id, item_id: item_id },
|
||||
onSuccess: function(res) {
|
||||
invoice_line = res.output;
|
||||
var l = invoice_line;
|
||||
$('#asset_name').val(l.product_name || l.product_sku);
|
||||
$('#acquisition_date, #in_service_date').val(format_date(l.issued_date));
|
||||
$('#cost').val(Math.max(parseFloat(l.remaining) || 0, 0).toFixed(2));
|
||||
$('#clearing_account_code').html(account_options(accounts, [], l.clearing_account_code, '— None —'));
|
||||
load_departments('department_id', l.department_id);
|
||||
$('#source_alert').removeClass('d-none').html(
|
||||
'<i class="ti ti-file-invoice me-1"></i>From purchase invoice <strong>' + escape_html(l.invoice_number) + '</strong>'
|
||||
+ ' (' + escape_html(l.contact_name || '—') + '), line <strong>' + escape_html(l.product_sku) + '</strong>: '
|
||||
+ 'amount ' + format_number(l.line_amount, 2) + ', already capitalized ' + format_number(l.capitalized, 2)
|
||||
+ ', remaining <strong>' + format_number(l.remaining, 2) + '</strong>.'
|
||||
+ (l.clearing_account_code ? '' : ' <span class="text-danger">Select the account the invoice line was posted to as the clearing account.</span>')
|
||||
);
|
||||
$('#page_subtitle').text('Capitalizing a purchase invoice line');
|
||||
update_calc();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_detail() {
|
||||
var a = detail.asset;
|
||||
var status = parseInt(a.status, 10);
|
||||
var recorded = detail.schedule.some(function(r) { return r.state === 'recorded'; });
|
||||
var closed = status === 3 || status === 4;
|
||||
|
||||
$('#page_title').text(a.asset_number + ' · ' + a.asset_name);
|
||||
$('#page_subtitle').text(a.category_code + ' ' + a.category_name);
|
||||
|
||||
$('#id').val(a.id);
|
||||
$('#asset_name').val(a.asset_name);
|
||||
fill_category_select(a.category_id);
|
||||
update_class_hint();
|
||||
$('#clearing_account_code').html(account_options(accounts, [], a.clearing_account_code, '— None —'));
|
||||
$('#acquisition_date').val(format_date(a.acquisition_date));
|
||||
$('#in_service_date').val(format_date(a.in_service_date));
|
||||
$('#cost').val(parseFloat(a.cost).toFixed(2));
|
||||
$('#salvage_value').val(parseFloat(a.salvage_value).toFixed(2));
|
||||
$('#useful_life_months').val(a.useful_life_months);
|
||||
$('#description').val(a.description || '');
|
||||
load_departments('department_id', a.department_id);
|
||||
life_touched = true;
|
||||
update_calc();
|
||||
|
||||
// Status card
|
||||
$('#status_card').removeClass('d-none');
|
||||
$('#status_badges').html(asset_status_badge(a.status, a.asset_class) + asset_class_badge(a.asset_class));
|
||||
$('#display_accumulated').text(format_number(a.accumulated, 2));
|
||||
$('#display_book_value').text(format_number(a.book_value, 2));
|
||||
$('#display_last_period').text(a.last_period || '—');
|
||||
if (parseInt(a.invoice_id, 10) > 0) {
|
||||
var ref = escape_html(a.invoice_number || ('PI #' + a.invoice_id));
|
||||
$('#display_source').html(pi_link_ok
|
||||
? '<a href="' + server_url + 'expense/manage_purchase_invoice.php?id=' + a.invoice_id + '">' + ref + '</a>'
|
||||
: ref);
|
||||
$('#source_row').removeClass('d-none');
|
||||
}
|
||||
|
||||
// GL entries
|
||||
var cap_html;
|
||||
if (!a.clearing_account_code) {
|
||||
cap_html = '<span class="text-muted">No entry — no clearing account.</span>';
|
||||
} else if (status === 0) {
|
||||
cap_html = '<span class="text-muted">Activate the asset, then Accounting posts it.</span>';
|
||||
} else {
|
||||
cap_html = gl_entry_html(detail.gl.capitalization);
|
||||
}
|
||||
$('#gl_capitalization').html(cap_html);
|
||||
$('#gl_disposal_row').toggleClass('d-none', status !== 3);
|
||||
$('#gl_disposal').html(gl_entry_html(detail.gl.disposal));
|
||||
|
||||
// Disposal
|
||||
if (status === 3) {
|
||||
var gain = round_dp(parseFloat(a.sale_price) - (parseFloat(a.cost) - parseFloat(a.accumulated)), 2);
|
||||
$('#display_disposal_date').text(format_date(a.disposal_date));
|
||||
$('#display_sale_price').text(format_number(a.sale_price, 2));
|
||||
$('#display_gain_loss').text(gain < 0 ? '(' + format_number(-gain, 2) + ')' : format_number(gain, 2))
|
||||
.toggleClass('text-danger', gain < 0).toggleClass('text-success', gain > 0);
|
||||
$('#display_disposal_notes').text(a.disposal_notes || '');
|
||||
$('#disposal_info').removeClass('d-none');
|
||||
} else {
|
||||
$('#disposal_info').addClass('d-none');
|
||||
}
|
||||
|
||||
render_schedule();
|
||||
|
||||
// Editing rules
|
||||
$(FINANCIAL_FIELDS).prop('disabled', closed || recorded || !can_edit);
|
||||
$('#asset_name, #description, #department_id').prop('disabled', closed || !can_edit);
|
||||
$('#locked_hint').toggleClass('d-none', !(recorded && !closed));
|
||||
|
||||
$('#btn_save').toggleClass('d-none', closed || !can_edit).find('span').text(status === 0 ? 'Save Draft' : 'Save');
|
||||
$('#btn_activate').toggleClass('d-none', !(status === 0 && can_admin));
|
||||
$('#btn_dispose').toggleClass('d-none', !((status === 1 || status === 2) && can_admin));
|
||||
$('#btn_undo_disposal').toggleClass('d-none', !(status === 3 && can_admin));
|
||||
$('#btn_void').toggleClass('d-none', !((status === 1 || status === 2) && can_admin && !recorded));
|
||||
$('#btn_delete').toggleClass('d-none', !(status === 0 && can_admin));
|
||||
}
|
||||
|
||||
function gl_entry_html(entry) {
|
||||
if (entry) {
|
||||
return '<span class="badge bg-success-subtle text-success">Posted</span>'
|
||||
+ ' <span class="text-muted ms-1">' + escape_html(entry.reference) + ' · ' + format_date(entry.journal_date) + '</span>';
|
||||
}
|
||||
return '<span class="badge bg-danger-subtle text-danger">Not posted</span>'
|
||||
+ ' <span class="text-muted ms-1">Accounting → Batch GL Entries</span>';
|
||||
}
|
||||
|
||||
function render_schedule() {
|
||||
var rows = detail.schedule || [];
|
||||
if (!rows.length) { $('#schedule_card').addClass('d-none'); return; }
|
||||
|
||||
var state_badge = {
|
||||
recorded: function(r) { return '<span class="badge bg-success-subtle text-success">Charged</span> <span class="small text-muted">' + escape_html(r.run_number || '') + '</span>'; },
|
||||
projected: function() { return '<span class="badge bg-light text-dark">Projected</span>'; },
|
||||
stopped: function() { return '<span class="badge bg-secondary-subtle text-secondary">Not charged</span>'; }
|
||||
};
|
||||
|
||||
var html = '';
|
||||
var charged = 0;
|
||||
rows.forEach(function(r, i) {
|
||||
if (r.state === 'recorded') charged++;
|
||||
html += '<tr class="' + (r.state === 'stopped' ? 'text-muted' : '') + '">'
|
||||
+ '<td>' + (i + 1) + '</td>'
|
||||
+ '<td>' + escape_html(r.period) + '</td>'
|
||||
+ '<td class="text-end">' + format_number(r.amount, 2) + '</td>'
|
||||
+ '<td class="text-end">' + format_number(r.accumulated, 2) + '</td>'
|
||||
+ '<td class="text-end">' + format_number(r.book_value, 2) + '</td>'
|
||||
+ '<td>' + state_badge[r.state](r) + '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
$('#schedule_tbody').html(html);
|
||||
$('#schedule_summary').text(charged + ' of ' + rows.length + ' months charged · ' + format_number(detail.monthly_amount, 2) + ' per month');
|
||||
$('#schedule_card').removeClass('d-none');
|
||||
}
|
||||
|
||||
// ── actions ────────────────────────────────────────────────────────────────
|
||||
function save_asset() {
|
||||
return ajax_request({
|
||||
url: server_url + 'asset/api/engine/manage_asset.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 1,
|
||||
action: 'manage',
|
||||
data: { id: asset_id || '', invoice_id: invoice_id, invoice_item_id: item_id },
|
||||
onSuccess: function(res) {
|
||||
var saved_id = res.output && res.output.id;
|
||||
if (!asset_id && saved_id) {
|
||||
window.location.href = server_url + 'asset/manage_asset.php?id=' + saved_id;
|
||||
return;
|
||||
}
|
||||
toastr.success(res.message || 'Saved.');
|
||||
retrieve_asset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var ACTION_CONFIRM = {
|
||||
activate: ['Activate this asset? It will start appearing in depreciation runs from its in-service month.', 'Activate', 'btn-success'],
|
||||
undo_disposal: ['Undo the disposal? If its GL entry was posted, a reversal entry is created.', 'Undo Disposal', 'btn-warning'],
|
||||
void: ['<strong>Void this asset?</strong><br>If its capitalization was posted, a reversal entry is created. This cannot be undone.', 'Void', 'btn-danger'],
|
||||
delete: ['Delete this draft asset?', 'Delete', 'btn-danger']
|
||||
};
|
||||
|
||||
function asset_action(action) {
|
||||
var c = ACTION_CONFIRM[action];
|
||||
bootbox.confirm({
|
||||
message: c[0],
|
||||
buttons: { confirm: { label: c[1], className: c[2] }, cancel: { label: 'Back', className: 'btn-secondary' } },
|
||||
callback: function(ok) {
|
||||
if (!ok) return;
|
||||
ajax_request({
|
||||
url: server_url + 'asset/api/engine/manage_asset.php',
|
||||
autoPrepare: false,
|
||||
action: action,
|
||||
data: { json: JSON.stringify({
|
||||
otp: document.getElementById('session-context').dataset.otp,
|
||||
company_id: company_id,
|
||||
action: action,
|
||||
id: asset_id
|
||||
}) },
|
||||
onSuccess: function(res) {
|
||||
if (action === 'delete') { window.location.href = server_url + 'asset/index.php'; return; }
|
||||
toastr.success(res.message || 'Done.');
|
||||
retrieve_asset();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function open_disposal() {
|
||||
var a = detail.asset;
|
||||
$('#proceeds_account_code').html(account_options(accounts, ['asset'], '', '— None (write-off) —'));
|
||||
$('#sale_price').val('0');
|
||||
$('#disposal_notes').val('');
|
||||
$('#dp_cost').text(format_number(a.cost, 2));
|
||||
$('#dp_accumulated').text(format_number(a.accumulated, 2));
|
||||
$('#dp_book').text(format_number(a.book_value, 2));
|
||||
update_disposal_result();
|
||||
$('#disposal_modal').modal('show');
|
||||
}
|
||||
|
||||
function update_disposal_result() {
|
||||
if (!detail) return;
|
||||
var gain = round_dp((parseFloat($('#sale_price').val()) || 0) - parseFloat(detail.asset.book_value), 2);
|
||||
$('#dp_result_label').text(gain < 0 ? 'Loss' : 'Gain');
|
||||
$('#dp_result').text(format_number(Math.abs(gain), 2)).toggleClass('text-danger', gain < 0).toggleClass('text-success', gain > 0);
|
||||
}
|
||||
|
||||
function submit_disposal() {
|
||||
ajax_request({
|
||||
url: server_url + 'asset/api/engine/manage_asset.php',
|
||||
autoPrepare: false,
|
||||
action: 'dispose',
|
||||
data: { json: JSON.stringify({
|
||||
otp: document.getElementById('session-context').dataset.otp,
|
||||
company_id: company_id,
|
||||
action: 'dispose',
|
||||
id: asset_id,
|
||||
disposal_date: $('#disposal_date').val(),
|
||||
sale_price: $('#sale_price').val(),
|
||||
proceeds_account_code: $('#proceeds_account_code').val(),
|
||||
disposal_notes: $('#disposal_notes').val()
|
||||
}) },
|
||||
onSuccess: function(res) {
|
||||
$('#disposal_modal').modal('hide');
|
||||
toastr.success(res.message || 'Disposal recorded.');
|
||||
retrieve_asset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── events ─────────────────────────────────────────────────────────────────
|
||||
$('#cost, #salvage_value, #useful_life_months').on('input', update_calc);
|
||||
$('#useful_life_months').on('input', function() { life_touched = true; });
|
||||
$('#category_id').on('change', function() {
|
||||
var c = category_by_id($(this).val());
|
||||
if (c && !life_touched) {
|
||||
$('#useful_life_months').val(c.useful_life_months);
|
||||
update_calc();
|
||||
}
|
||||
update_class_hint();
|
||||
});
|
||||
$('#sale_price').on('input', update_disposal_result);
|
||||
|
||||
$(async function() {
|
||||
flatpickr('#acquisition_date, #in_service_date, #disposal_date', { dateFormat: 'd/m/Y', allowInput: true });
|
||||
|
||||
try {
|
||||
await load_categories();
|
||||
await load_posting_accounts(function(rows) { accounts = rows; });
|
||||
fill_category_select('');
|
||||
$('#clearing_account_code').html(account_options(accounts, [], '', '— None —'));
|
||||
|
||||
if (asset_id) {
|
||||
await retrieve_asset();
|
||||
} else {
|
||||
load_departments('department_id', 0);
|
||||
if (invoice_id && item_id) await load_invoice_line();
|
||||
if (!can_edit) $('#btn_save').addClass('d-none');
|
||||
}
|
||||
} catch (e) { console.log(e); }
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user