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.
174 lines
7.0 KiB
PHP
174 lines
7.0 KiB
PHP
<?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();
|
|
}
|
|
}
|