Files
wms-app/app/asset/depreciation.php
T
Thanakorn c957280aa5 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.
2026-09-15 09:50:55 +07:00

318 lines
13 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<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-5">
<h1 class="fs-3 mb-1">Depreciation &amp; Amortization Runs</h1>
<p class="mb-0">Charge each month's straight-line depreciation (tangible) or amortization (intangible)</p>
</div>
</div>
</div>
<!-- New run -->
<div class="card mb-5" data-min-role="admin">
<div class="card-body p-5">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label mb-1">Type</label>
<select id="asset_class" class="form-select">
<option value="tangible">Depreciation — tangible assets</option>
<option value="intangible">Amortization — intangible assets</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label mb-1">Run Through Month</label>
<input type="text" id="through_period" class="form-control" placeholder="YYYY-MM">
</div>
<div class="col-md-5 d-flex gap-2">
<button class="btn btn-secondary" onclick="preview_run()"><i class="ti ti-eye me-1"></i>Preview</button>
<button class="btn btn-primary" id="btn_run" onclick="create_run()" disabled><i class="ti ti-player-play me-1"></i>Create Runs</button>
</div>
</div>
<p class="text-muted small mt-3 mb-0">
One run is created per month still due, oldest first. The in-service month counts as a full month and the
disposal month is not charged. Each run becomes one GL entry that Accounting posts from
<strong>Batch GL Entries</strong>, labelled <strong>[Asset Mgmt]</strong>.
</p>
<div id="preview_area" class="mt-4"></div>
</div>
</div>
<!-- Runs -->
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4 gap-3">
<h2 class="fs-5 mb-0">Runs</h2>
<select id="filter_class" class="form-select form-select-sm" style="width:220px;">
<option value="">All types</option>
<option value="tangible">Depreciation</option>
<option value="intangible">Amortization</option>
</select>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered text-nowrap" id="run_table">
<thead class="table-primary border-light">
<tr>
<th>Run No.</th>
<th>Type</th>
<th>Month</th>
<th>Run Date</th>
<th class="text-end">Assets</th>
<th class="text-end">Amount</th>
<th>GL</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td colspan="9" class="text-center text-muted py-5">Loading...</td></tr>
</tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</main>
<!-- Run detail -->
<div class="modal fade" id="run_detail_modal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="run_detail_title">Run</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="run_detail_body"></div>
</div>
</div>
</div>
<?php require '../include_ending.php'; ?>
<script src="<?php echo $server_url?>assets/js/asset.js"></script>
<script>
var can_admin = ['owner', 'admin'].includes(user_role);
var all_runs = [];
var last_preview = null;
function run_type_label(asset_class) {
return asset_class === 'intangible' ? 'Amortization' : 'Depreciation';
}
function preview_run() {
last_preview = null;
set_btn_state('#btn_run', false);
return ajax_request({
url: server_url + 'asset/api/engine/depreciation_run.php',
autoPrepare: true,
checkRequired: 0,
action: 'preview',
data: { asset_class: $('#asset_class').val(), through_period: $('#through_period').val() },
onSuccess: function(res) {
var runs = res.output || [];
last_preview = { asset_class: $('#asset_class').val(), through_period: $('#through_period').val(), runs: runs };
render_preview(runs);
set_btn_state('#btn_run', runs.length > 0);
}
});
}
function render_preview(runs) {
if (!runs.length) {
$('#preview_area').html('<div class="alert alert-success small mb-0"><i class="ti ti-circle-check me-1"></i>Nothing is due through this month.</div>');
return;
}
var total = 0;
var html = '<div class="table-responsive"><table class="table table-sm mb-0 table-centered">'
+ '<thead class="table-light"><tr><th>Month</th><th>Run Date</th><th class="text-end">Assets</th><th class="text-end">Amount</th><th></th></tr></thead><tbody>';
runs.forEach(function(r, idx) {
total += parseFloat(r.total) || 0;
html += '<tr>'
+ '<td class="fw-semibold">' + escape_html(r.period) + '</td>'
+ '<td>' + format_date(r.run_date) + '</td>'
+ '<td class="text-end">' + r.asset_count + '</td>'
+ '<td class="text-end">' + format_number(r.total, 2) + '</td>'
+ '<td class="text-end"><a href="javascript:;" class="small" onclick="$(\'#preview_items_' + idx + '\').toggleClass(\'d-none\')">Assets</a></td>'
+ '</tr>'
+ '<tr id="preview_items_' + idx + '" class="d-none"><td colspan="5" class="bg-light">'
+ items_table(r.items, false)
+ '</td></tr>';
});
html += '</tbody><tfoot><tr class="fw-semibold"><td colspan="3">Total</td><td class="text-end">' + format_number(total, 2) + '</td><td></td></tr></tfoot></table></div>';
$('#preview_area').html(html);
}
function items_table(items, with_dept) {
var html = '<table class="table table-sm mb-0"><thead><tr><th>Asset</th><th>Category</th>'
+ (with_dept ? '<th>Dept</th>' : '')
+ '<th class="text-end">Amount</th><th class="text-end">Accumulated</th><th class="text-end">Book Value</th></tr></thead><tbody>';
(items || []).forEach(function(i) {
html += '<tr>'
+ '<td>' + escape_html(i.asset_number) + ' <span class="text-muted">' + escape_html(i.asset_name) + '</span></td>'
+ '<td>' + escape_html((i.category_code ? i.category_code + ' ' : '') + (i.category_name || '')) + '</td>'
+ (with_dept ? '<td>' + escape_html(i.dept_code || '—') + '</td>' : '')
+ '<td class="text-end">' + format_number(i.amount, 2) + '</td>'
+ '<td class="text-end">' + format_number(i.accumulated_after, 2) + '</td>'
+ '<td class="text-end">' + format_number(i.book_value_after, 2) + '</td>'
+ '</tr>';
});
return html + '</tbody></table>';
}
function create_run() {
if (!last_preview || !last_preview.runs.length) return;
var total = last_preview.runs.reduce(function(s, r) { return s + (parseFloat(r.total) || 0); }, 0);
bootbox.confirm({
message: 'Create <strong>' + last_preview.runs.length + '</strong> ' + run_type_label(last_preview.asset_class).toLowerCase()
+ ' run(s) through ' + escape_html(last_preview.through_period) + ', totalling <strong>' + format_number(total, 2) + '</strong>?',
buttons: { confirm: { label: 'Create Runs', className: 'btn-primary' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(ok) {
if (!ok) return;
ajax_request({
url: server_url + 'asset/api/engine/depreciation_run.php',
autoPrepare: true,
checkRequired: 0,
action: 'run',
data: { asset_class: last_preview.asset_class, through_period: last_preview.through_period },
onSuccess: function(res) {
toastr.success(res.message || 'Runs created.');
$('#preview_area').empty();
set_btn_state('#btn_run', false);
last_preview = null;
load_runs();
}
});
}
});
}
function load_runs() {
return ajax_request({
url: server_url + 'asset/api/engine/depreciation_run.php',
autoPrepare: true,
checkRequired: 0,
action: 'list',
data: { asset_class: '' },
onSuccess: function(res) {
all_runs = res.output || [];
change_page(1);
}
});
}
function change_page(page_num) {
var filter = $('#filter_class').val();
var rows = all_runs.filter(function(r) { return !filter || r.asset_class === filter; });
var limit = parseInt(prop_limit, 10);
var offset = (page_num - 1) * limit;
$('#run_table tfoot').html(generate_pagination('run_table', rows.length));
if (!rows.length) {
$('#run_table tbody').html('<tr><td colspan="9" class="text-center text-muted py-5">No runs yet.</td></tr>');
return;
}
var html = '';
rows.slice(offset, offset + limit).forEach(function(r) {
var active = String(r.status) === '1';
html += '<tr class="' + (active ? '' : 'text-muted') + '">'
+ '<td class="fw-semibold">' + escape_html(r.run_number) + '</td>'
+ '<td>' + run_type_label(r.asset_class) + '</td>'
+ '<td>' + escape_html(r.period) + '</td>'
+ '<td>' + format_date(r.run_date) + '</td>'
+ '<td class="text-end">' + r.asset_count + '</td>'
+ '<td class="text-end">' + format_number(r.total_amount, 2) + '</td>'
+ '<td>' + (active ? asset_gl_badge(r.gl_status) : '—') + '</td>'
+ '<td>' + (active ? '<span class="badge bg-success">Active</span>' : '<span class="badge bg-light text-dark">Void</span>') + '</td>'
+ '<td class="text-end">'
+ (active ? '<a href="javascript:;" onclick="show_run(' + r.id + ')"><i class="ti ti-eye fs-5"></i></a>' : '')
+ (active && can_admin ? '<a href="javascript:;" class="link-danger ms-2" title="Void run" onclick="void_run(' + r.id + ', \'' + escape_html(r.run_number) + '\', ' + r.gl_status + ')"><i class="ti ti-ban fs-5"></i></a>' : '')
+ '</td></tr>';
});
$('#run_table tbody').html(html);
}
function show_run(run_id) {
$('#run_detail_body').html('<div class="text-center text-muted py-5">Loading...</div>');
$('#run_detail_modal').modal('show');
ajax_request({
url: server_url + 'asset/api/engine/depreciation_run.php',
autoPrepare: true,
checkRequired: 0,
action: 'detail',
data: { run_id: run_id },
onSuccess: function(res) {
var run = res.output.run;
$('#run_detail_title').text(run.run_number + ' · ' + run_type_label(run.asset_class) + ' ' + run.period);
$('#run_detail_body').html(
'<div class="small text-muted mb-3">Run date ' + format_date(run.run_date) + ' · '
+ run.asset_count + ' assets · total ' + format_number(run.total_amount, 2) + '</div>'
+ items_table(res.output.items, true)
);
}
});
}
function void_run(run_id, run_number, gl_status) {
var note = String(gl_status) === '1'
? '<br><span class="text-danger">Its GL entry is posted, so a reversal entry will be created.</span>'
: '';
bootbox.confirm({
message: '<strong>Void run ' + run_number + '?</strong><br>Its charges are removed from the assets.' + note,
buttons: { confirm: { label: 'Void', className: 'btn-danger' }, cancel: { label: 'Back', className: 'btn-secondary' } },
callback: function(ok) {
if (!ok) return;
ajax_request({
url: server_url + 'asset/api/engine/depreciation_run.php',
autoPrepare: true,
checkRequired: 0,
action: 'void',
data: { run_id: run_id },
onSuccess: function(res) {
toastr.success(res.message || 'Run voided.');
load_runs();
}
});
}
});
}
$('#filter_class').on('change', function() { change_page(1); });
$('#asset_class, #through_period').on('change', function() {
last_preview = null;
set_btn_state('#btn_run', false);
$('#preview_area').empty();
});
$(async function() {
var params = new URLSearchParams(window.location.search);
if (params.get('class') === 'intangible' || params.get('class') === 'tangible') {
$('#asset_class').val(params.get('class'));
}
flatpickr('#through_period', {
plugins: [new monthSelectPlugin({ shorthand: true, dateFormat: 'Y-m', altFormat: 'M Y' })],
defaultDate: current_period() + '-01',
maxDate: new Date()
});
$('#through_period').val(current_period());
try { await load_runs(); } catch (e) { console.log(e); }
});
</script>
</body>
</html>