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,173 @@
|
||||
<?php
|
||||
/**
|
||||
* AssetCategoryManager
|
||||
*
|
||||
* Asset categories decide the asset class (tangible = depreciation,
|
||||
* intangible = amortization), the default useful life, and every GL account
|
||||
* the Asset Management postings use.
|
||||
*/
|
||||
class AssetCategoryManager
|
||||
{
|
||||
public const CLASSES = ['tangible', 'intangible'];
|
||||
|
||||
public const ACCOUNT_FIELDS = [
|
||||
'cost_account_code' => 'Asset cost account',
|
||||
'accum_account_code' => 'Accumulated depreciation / amortization account',
|
||||
'expense_account_code' => 'Depreciation / amortization expense account',
|
||||
'gain_account_code' => 'Gain on disposal account',
|
||||
'loss_account_code' => 'Loss on disposal account',
|
||||
];
|
||||
|
||||
private PDO $pdo;
|
||||
private int $companyId;
|
||||
|
||||
public function __construct(PDO $pdo, int $company_id)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
$this->companyId = $company_id;
|
||||
}
|
||||
|
||||
public static function isPostingAccount(PDO $pdo, int $company_id, string $account_code): bool
|
||||
{
|
||||
if ($account_code === '') return false;
|
||||
$sth = $pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_account
|
||||
WHERE company_id = :cid AND account_code = :code AND is_posting = 1 AND status = 1"
|
||||
);
|
||||
$sth->execute([':cid' => $company_id, ':code' => $account_code]);
|
||||
return (int)$sth->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT c.*, COUNT(a.id) AS asset_count
|
||||
FROM md_asset_category c
|
||||
LEFT JOIN td_asset a
|
||||
ON a.company_id = c.company_id
|
||||
AND a.category_id = c.id
|
||||
AND a.status <> 4
|
||||
WHERE c.company_id = :cid
|
||||
GROUP BY c.id
|
||||
ORDER BY c.category_code ASC"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function getById(int $id): ?array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM md_asset_category WHERE company_id = :cid AND id = :id LIMIT 1"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId, ':id' => $id]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
}
|
||||
|
||||
public function save(array $data): int
|
||||
{
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$code = strtoupper(trim((string)($data['category_code'] ?? '')));
|
||||
$name = trim((string)($data['category_name'] ?? ''));
|
||||
$class = trim((string)($data['asset_class'] ?? ''));
|
||||
$life = (int)($data['useful_life_months'] ?? 0);
|
||||
|
||||
if ($code === '' || $name === '') throw new Exception('Category code and name are required.');
|
||||
if (!in_array($class, self::CLASSES, true)) throw new Exception('Select tangible or intangible.');
|
||||
if ($life < 1 || $life > 1200) throw new Exception('Default useful life must be between 1 and 1200 months.');
|
||||
|
||||
$accounts = [];
|
||||
foreach (self::ACCOUNT_FIELDS as $field => $label) {
|
||||
$account_code = trim((string)($data[$field] ?? ''));
|
||||
if ($account_code === '') throw new Exception("{$label} is required.");
|
||||
if (!self::isPostingAccount($this->pdo, $this->companyId, $account_code)) {
|
||||
throw new Exception("{$label} {$account_code} is not an active posting account.");
|
||||
}
|
||||
$accounts[$field] = $account_code;
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_asset_category
|
||||
WHERE company_id = :cid AND category_code = :code AND id <> :id"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId, ':code' => $code, ':id' => $id]);
|
||||
if ((int)$sth->fetchColumn() > 0) throw new Exception("Category code {$code} already exists.");
|
||||
|
||||
$params = array_merge([
|
||||
':cid' => $this->companyId,
|
||||
':code' => $code,
|
||||
':name' => $name,
|
||||
':class' => $class,
|
||||
':life' => $life,
|
||||
':desc' => trim((string)($data['description'] ?? '')),
|
||||
':status' => (int)($data['status'] ?? 1) === 1 ? 1 : 0,
|
||||
], [
|
||||
':cost_acc' => $accounts['cost_account_code'],
|
||||
':accum_acc' => $accounts['accum_account_code'],
|
||||
':expense_acc' => $accounts['expense_account_code'],
|
||||
':gain_acc' => $accounts['gain_account_code'],
|
||||
':loss_acc' => $accounts['loss_account_code'],
|
||||
]);
|
||||
|
||||
if ($id) {
|
||||
$existing = $this->getById($id);
|
||||
if (!$existing) throw new Exception('Category not found.');
|
||||
if ($existing['asset_class'] !== $class && $this->countAssets($id) > 0) {
|
||||
throw new Exception('The asset class cannot change once assets use this category.');
|
||||
}
|
||||
|
||||
$params[':id'] = $id;
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_asset_category SET
|
||||
category_code = :code, category_name = :name, asset_class = :class,
|
||||
useful_life_months = :life, description = :desc, status = :status,
|
||||
cost_account_code = :cost_acc, accum_account_code = :accum_acc,
|
||||
expense_account_code = :expense_acc, gain_account_code = :gain_acc,
|
||||
loss_account_code = :loss_acc, updated_at = NOW()
|
||||
WHERE company_id = :cid AND id = :id"
|
||||
)->execute($params);
|
||||
return $id;
|
||||
}
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_asset_category
|
||||
(company_id, category_code, category_name, asset_class, useful_life_months, description, status,
|
||||
cost_account_code, accum_account_code, expense_account_code, gain_account_code, loss_account_code,
|
||||
created_at)
|
||||
VALUES
|
||||
(:cid, :code, :name, :class, :life, :desc, :status,
|
||||
:cost_acc, :accum_acc, :expense_acc, :gain_acc, :loss_acc,
|
||||
NOW())"
|
||||
)->execute($params);
|
||||
return (int)$this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/** Categories are only deactivated: existing assets keep their accounts. */
|
||||
public function deactivate(int $id): void
|
||||
{
|
||||
$this->pdo->prepare(
|
||||
"UPDATE md_asset_category SET status = 0, updated_at = NOW() WHERE company_id = :cid AND id = :id"
|
||||
)->execute([':cid' => $this->companyId, ':id' => $id]);
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) AS total,
|
||||
COALESCE(SUM(status = 1), 0) AS active,
|
||||
COALESCE(SUM(status = 0), 0) AS inactive
|
||||
FROM md_asset_category WHERE company_id = :cid"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId]);
|
||||
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
private function countAssets(int $category_id): int
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM td_asset WHERE company_id = :cid AND category_id = :id AND status <> 4"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId, ':id' => $category_id]);
|
||||
return (int)$sth->fetchColumn();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user