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:
Thanakorn
2026-09-15 09:50:55 +07:00
parent 9512d440dd
commit c957280aa5
46 changed files with 4316 additions and 131 deletions
+74
View File
@@ -0,0 +1,74 @@
// Shared helpers for the Asset Management pages.
var ASSET_STATUS = {
0: ['Draft', 'bg-secondary'],
1: ['Active', 'bg-success'],
2: ['Fully depreciated', 'bg-info text-white'],
3: ['Disposed', 'bg-dark'],
4: ['Void', 'bg-light text-dark']
};
function asset_status_badge(status, asset_class) {
var code = parseInt(status, 10);
var entry = ASSET_STATUS[code] || ['—', 'bg-light text-dark'];
var label = (code === 2 && asset_class === 'intangible') ? 'Fully amortized' : entry[0];
return '<span class="badge ' + entry[1] + '">' + label + '</span>';
}
function asset_class_badge(asset_class) {
return asset_class === 'intangible'
? '<span class="badge bg-warning-subtle text-warning">Intangible · Amortization</span>'
: '<span class="badge bg-success-subtle text-success">Tangible · Depreciation</span>';
}
function asset_gl_badge(posted) {
return String(posted) === '1'
? '<span class="badge bg-success-subtle text-success">Posted</span>'
: '<span class="badge bg-danger-subtle text-danger">Unposted</span>';
}
// Active posting accounts from the chart of accounts.
function load_posting_accounts(onLoaded) {
return ajax_request({
url: server_url + 'accounting/api/engine/account.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
queueLock: false,
noLoading: true,
onSuccess: function(res) {
var rows = (res.output || []).filter(function(a) {
return String(a.is_posting) === '1' && String(a.status) === '1';
});
onLoaded(rows);
}
});
}
// <option> list for accounts, narrowed to account_types when any match.
function account_options(accounts, account_types, selected, empty_label) {
var list = accounts;
if (account_types && account_types.length) {
var narrowed = accounts.filter(function(a) { return account_types.indexOf(a.account_type) !== -1; });
if (narrowed.length) list = narrowed;
}
var html = '<option value="">' + escape_html(empty_label || '— Select account —') + '</option>';
var found = false;
list.forEach(function(a) {
var is_selected = String(a.account_code) === String(selected || '');
if (is_selected) found = true;
html += '<option value="' + escape_html(a.account_code) + '"' + (is_selected ? ' selected' : '') + '>'
+ escape_html(a.account_code + ' — ' + a.account_name) + '</option>';
});
// Keep a saved code visible even if it is no longer an active posting account.
if (selected && !found) {
html += '<option value="' + escape_html(selected) + '" selected>' + escape_html(selected) + ' (not active)</option>';
}
return html;
}
function current_period() {
var d = new Date();
return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
}