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
+18 -5
View File
@@ -11,6 +11,7 @@ require_once '../../../assets/utils/classes_ac/posting/SupplierCreditNotePosting
require_once '../../../assets/utils/classes_ac/posting/ReceiptPosting.php';
require_once '../../../assets/utils/classes_ac/posting/PaymentPosting.php';
require_once '../../../assets/utils/classes_ac/posting/PurchasePosting.php';
require_once '../../../assets/utils/classes_as/AssetPosting.php';
require_role($user_role, ['owner', 'admin']);
require_once '../../../assets/utils/notify_node.php';
@@ -29,19 +30,31 @@ $posting_map = [
'purchase_order' => PurchasePosting::class,
];
if (!isset($posting_map[$doc_type]) || $id <= 0) {
$is_asset_source = AssetPosting::isAssetSource($doc_type);
if ((!isset($posting_map[$doc_type]) && !$is_asset_source) || $id <= 0) {
$answer['message'] = 'Invalid doc_type or id.';
exit(json_encode($answer));
}
try {
$posting_class = $posting_map[$doc_type];
$posting = new $posting_class($pdo2, $company_id);
$built = $posting->build($id, $formula_id);
if ($is_asset_source) {
// Asset Management entries carry their own reference and "[Asset Mgmt]" description.
$built = (new AssetPosting($pdo2, $company_id))->build($doc_type, $id);
$meta = [
'journal_date' => $built['doc_date'],
'reference' => $built['reference'],
'description' => $built['description'],
];
} else {
$posting_class = $posting_map[$doc_type];
$posting = new $posting_class($pdo2, $company_id);
$built = $posting->build($id, $formula_id);
$meta = ['journal_date' => $built['doc_date'] ?? null];
}
$guard = new PostingWindowGuard($pdo1, $company_id);
$gl = new GlManager($pdo2, $company_id, $guard);
$meta = ['journal_date' => $built['doc_date'] ?? null];
$pdo2->beginTransaction();
+82 -18
View File
@@ -39,6 +39,22 @@
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-payment">Payment</a>
</li>
<!-- Entries prepared in Asset Management -->
<li class="nav-item d-flex align-items-center ms-lg-3 px-2">
<span class="badge bg-success text-white"><i class="ti ti-building-warehouse me-1"></i>Asset Management</span>
</li>
<li class="nav-item">
<a class="nav-link text-success" data-bs-toggle="tab" href="#tab-asset-capitalization">Capitalization</a>
</li>
<li class="nav-item">
<a class="nav-link text-success" data-bs-toggle="tab" href="#tab-asset-depreciation">Depreciation</a>
</li>
<li class="nav-item">
<a class="nav-link text-success" data-bs-toggle="tab" href="#tab-asset-amortization">Amortization</a>
</li>
<li class="nav-item">
<a class="nav-link text-success" data-bs-toggle="tab" href="#tab-asset-disposal">Disposal</a>
</li>
</ul>
<div class="tab-content">
@@ -60,6 +76,18 @@
<div class="tab-pane fade" id="tab-payment">
<?php echo gl_tab_html('payment', 'Payment'); ?>
</div>
<div class="tab-pane fade" id="tab-asset-capitalization">
<?php echo gl_tab_html('asset_capitalization', 'Asset Capitalization', true); ?>
</div>
<div class="tab-pane fade" id="tab-asset-depreciation">
<?php echo gl_tab_html('asset_depreciation', 'Asset Depreciation', true); ?>
</div>
<div class="tab-pane fade" id="tab-asset-amortization">
<?php echo gl_tab_html('asset_amortization', 'Asset Amortization', true); ?>
</div>
<div class="tab-pane fade" id="tab-asset-disposal">
<?php echo gl_tab_html('asset_disposal', 'Asset Disposal', true); ?>
</div>
</div>
</div>
</div>
@@ -76,9 +104,17 @@
'purchase_invoice': 'purchase_invoice',
'supplier_credit_note': 'supplier_credit_note',
'receipt': 'receipt',
'payment': 'payment'
'payment': 'payment',
'asset_capitalization': 'asset_capitalization',
'asset_depreciation': 'asset_depreciation',
'asset_amortization': 'asset_amortization',
'asset_disposal': 'asset_disposal'
};
// Prepared in Asset Management: no GL formula, accounts come from the asset category.
var ASSET_TYPES = ['asset_capitalization', 'asset_depreciation', 'asset_amortization', 'asset_disposal'];
function is_asset_type(doc_type) { return ASSET_TYPES.indexOf(doc_type) !== -1; }
var _gl_loaded = {}; // doc_type → [ids] (flat, used by single-formula run)
var _gl_rows = {}; // doc_type → [rows]
var _gl_failed = {}; // doc_type → failed items from last run
@@ -91,11 +127,13 @@
if (_inited[doc_type]) return;
_inited[doc_type] = true;
load_formula_options('gl-formula-' + doc_type, doc_type, null, function(formulas) {
var $sel = $('#gl-formula-' + doc_type);
$sel.prepend('<option value="all">— All Formulas (' + formulas.length + ') —</option>');
$sel.val('all');
});
if (!is_asset_type(doc_type)) {
load_formula_options('gl-formula-' + doc_type, doc_type, null, function(formulas) {
var $sel = $('#gl-formula-' + doc_type);
$sel.prepend('<option value="all">— All Formulas (' + formulas.length + ') —</option>');
$sel.val('all');
});
}
var today = new Date();
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
@@ -117,7 +155,7 @@
// ── load documents ────────────────────────────────────────────────────────
function load_gl_tab(doc_type) {
var formula_id = $('#gl-formula-' + doc_type).val();
var formula_id = is_asset_type(doc_type) ? '0' : $('#gl-formula-' + doc_type).val();
if (!formula_id) { bootbox.alert('Please select a GL formula first.'); return; }
if (formula_id === 'all') {
@@ -256,7 +294,9 @@
var status_badge = r.gl_status == 1
? '<span class="badge bg-success-subtle text-success">Posted</span>'
: '<span class="badge bg-danger-subtle text-danger">Unposted</span>';
var mapping_badge = product_mapping_badge(r);
var mapping_badge = is_asset_type(doc_type)
? '<span class="badge bg-success-subtle text-success">Asset category</span>'
: product_mapping_badge(r);
html +=
'<tr>' +
'<td>' + escape_html(r.doc_number) + '</td>' +
@@ -302,11 +342,15 @@
purchase_invoice: 'Purchase Invoice',
supplier_credit_note: 'Supplier Credit Note',
receipt: 'Receipt',
payment: 'Payment'
payment: 'Payment',
asset_capitalization: '[Asset Mgmt] Capitalization',
asset_depreciation: '[Asset Mgmt] Depreciation',
asset_amortization: '[Asset Mgmt] Amortization',
asset_disposal: '[Asset Mgmt] Disposal'
};
function run_gl_batch(doc_type) {
var formula_id = $('#gl-formula-' + doc_type).val();
var formula_id = is_asset_type(doc_type) ? '0' : $('#gl-formula-' + doc_type).val();
if (formula_id === 'all') {
run_all_formula_batches(doc_type);
@@ -393,7 +437,7 @@
// ── core batch processor ──────────────────────────────────────────────────
function _start_gl_batch(doc_type, ids, onComplete, formula_id_override) {
var formula_id = formula_id_override || $('#gl-formula-' + doc_type).val();
var formula_id = formula_id_override || (is_asset_type(doc_type) ? 0 : $('#gl-formula-' + doc_type).val());
var $msg = $('#gl-msg-' + doc_type);
set_btn_state('#gl-btn-load-' + doc_type, false);
@@ -516,10 +560,34 @@
</html>
<?php
function gl_tab_html(string $doc_type, string $label): string {
function gl_tab_html(string $doc_type, string $label, bool $from_asset = false): string {
// Asset Management entries have no GL formula: say where they come from instead.
$source_field = $from_asset
? <<<HTML
<div class="col-auto">
<label class="form-label mb-1 small">Source</label>
<div><span class="badge bg-success text-white py-2 px-3"><i class="ti ti-building-warehouse me-1"></i>From Asset Management</span></div>
</div>
HTML
: <<<HTML
<div class="col-auto">
<label class="form-label mb-1 small">GL Formula <span class="text-danger">*</span></label>
<select id="gl-formula-{$doc_type}" class="form-select form-select-sm" style="width:220px;">
</select>
</div>
HTML;
$source_note = $from_asset
? '<div class="alert alert-success small py-2 mb-3"><i class="ti ti-info-circle me-1"></i>'
. 'Prepared in <strong>Asset Management</strong>. Accounts come from each asset category; '
. 'entries are labelled <strong>[Asset Mgmt]</strong> in the GL Journal.</div>'
: '';
$accounts_header = $from_asset ? 'Accounts' : 'Product Accounts';
return <<<HTML
<div class="card p-4">
{$source_note}
<!-- Filters + action buttons -->
<div class="row g-2 align-items-end mb-3">
<div class="col-auto">
@@ -532,11 +600,7 @@ function gl_tab_html(string $doc_type, string $label): string {
<input type="text" id="gl-date-to-{$doc_type}" class="form-control form-control-sm"
placeholder="d/m/Y" style="width:140px;">
</div>
<div class="col-auto">
<label class="form-label mb-1 small">GL Formula <span class="text-danger">*</span></label>
<select id="gl-formula-{$doc_type}" class="form-select form-select-sm" style="width:220px;">
</select>
</div>
{$source_field}
<div class="col-auto">
<button id="gl-btn-load-{$doc_type}" class="btn btn-secondary btn-sm"
onclick="load_gl_tab('{$doc_type}')">
@@ -565,7 +629,7 @@ function gl_tab_html(string $doc_type, string $label): string {
<th>Contact</th>
<th class="text-end">Amount</th>
<th>Date</th>
<th>Product Accounts</th>
<th>{$accounts_header}</th>
<th>GL Status</th>
</tr>
</thead>
+17 -3
View File
@@ -48,6 +48,12 @@
<option value="payment">Payment</option>
<option value="manual">Manual</option>
<option value="reversal">Reversal</option>
<optgroup label="Asset Management">
<option value="asset_capitalization">[Asset Mgmt] Capitalization</option>
<option value="asset_depreciation">[Asset Mgmt] Depreciation</option>
<option value="asset_amortization">[Asset Mgmt] Amortization</option>
<option value="asset_disposal">[Asset Mgmt] Disposal</option>
</optgroup>
</select>
</div>
<div class="col-auto">
@@ -188,7 +194,11 @@
receipt: 'Receipt',
payment: 'Payment',
manual: 'Manual',
reversal: 'Reversal'
reversal: 'Reversal',
asset_capitalization: 'Asset Mgmt · Capitalization',
asset_depreciation: 'Asset Mgmt · Depreciation',
asset_amortization: 'Asset Mgmt · Amortization',
asset_disposal: 'Asset Mgmt · Disposal'
};
var SOURCE_BADGE = {
@@ -199,7 +209,11 @@
receipt: 'bg-info-subtle text-info',
payment: 'bg-danger-subtle text-danger',
manual: 'bg-dark-subtle text-dark',
reversal: 'bg-danger-subtle text-danger'
reversal: 'bg-danger-subtle text-danger',
asset_capitalization: 'bg-success text-white',
asset_depreciation: 'bg-success text-white',
asset_amortization: 'bg-success text-white',
asset_disposal: 'bg-success text-white'
};
function source_badge(type) {
@@ -507,7 +521,7 @@
? '<span class="badge bg-success-subtle text-success ms-2">Balanced</span>'
: '<span class="badge bg-danger-subtle text-danger ms-2">Unbalanced</span>';
var extra_fields = h.source_type === 'manual'
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">Journal Date</div><div>' + escape_html(h.journal_date_fmt || '—') + '</div></div>' +
'<div class="col-sm-4"><div class="text-muted small">Description</div><div>' + escape_html(h.description || '—') + '</div></div>'
+2
View File
@@ -139,6 +139,8 @@
invoice: 'Invoice', credit_note: 'Credit Note',
purchase_invoice: 'Purchase Invoice', supplier_credit_note: 'Supplier Credit Note',
receipt: 'Receipt', payment: 'Payment', manual: 'Manual',
asset_capitalization: 'Asset Mgmt · Capitalization', asset_depreciation: 'Asset Mgmt · Depreciation',
asset_amortization: 'Asset Mgmt · Amortization', asset_disposal: 'Asset Mgmt · Disposal',
};
load_departments();