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.
318 lines
13 KiB
PHP
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 & 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>
|