Commit afa29392 authored by DevPilot's avatar DevPilot

feat(inventory): add Asset Maintenance tracking with GL posting

Closes the remaining gap in the fixed-asset lifecycle (acquisition,
depreciation, disposal/sale already existed) by adding a maintenance
log per asset with cost, vendor, and next-due tracking. Each category
now carries its own maintenance expense account, mirroring how
depreciation and disposal are already mapped, and maintenance cost
posts Dr expense / Cr cash-bank-payable through the same operational
posting pipeline.
parent 89695e0e
......@@ -406,6 +406,86 @@ final class OperationalPostingService
}
}
/**
* Maintenance is a real expense of the period it happens in — unlike a
* purchase it is not capitalised, because it keeps the asset running rather
* than adding to what it is worth.
*
* Dr مصروف صيانة (فئة الأصل)
* Cr النقدية / البنك / الموردين
*/
public static function onAssetMaintenanceRecorded(array $data): void
{
$maintenanceId = (int) ($data['maintenance_id'] ?? 0);
if ($maintenanceId <= 0) {
return;
}
if (JournalEntry::findByReference('asset_maintenance', $maintenanceId)) {
return;
}
$db = App::getInstance()->db();
$maintenance = $db->selectOne(
"SELECT m.id, m.asset_id, m.cost, m.maintenance_date, m.description,
a.asset_tag, a.branch_id, a.category_id,
c.maintenance_account_id
FROM asset_maintenance m
JOIN asset_register a ON a.id = m.asset_id
LEFT JOIN asset_categories c ON c.id = a.category_id
WHERE m.id = ?",
[$maintenanceId]
);
if (!$maintenance) {
return;
}
$cost = self::money((string) ($data['cost'] ?? $maintenance['cost'] ?? '0'));
if (bccomp($cost, '0.00', self::SCALE) <= 0) {
return;
}
$expenseAccount = (int) ($maintenance['maintenance_account_id'] ?? 0);
if ($expenseAccount <= 0) {
Logger::error('Asset maintenance not posted — category has no maintenance expense account', [
'maintenance_id' => $maintenanceId,
'category' => $maintenance['category_id'] ?? null,
]);
return;
}
$creditAccount = match ((string) ($data['payment_method'] ?? 'cash')) {
'bank' => PostingRouter::accountFor('treasury:method_bank_transfer', AccountCodes::CASH_AT_BANK, 'collection'),
'payable' => PostingRouter::accountFor('procurement:payable', AccountCodes::ACCOUNTS_PAYABLE, 'accrual'),
default => PostingRouter::accountFor('treasury:method_cash', AccountCodes::CASH_ON_HAND, 'collection'),
};
if ($creditAccount === null) {
Logger::error('Asset maintenance not posted — funding account unresolved', ['maintenance_id' => $maintenanceId]);
return;
}
$name = $maintenance['asset_tag'] ?: ('#' . (int) $maintenance['asset_id']);
$desc = 'صيانة أصل — ' . $name . ' — ' . $maintenance['description'];
$result = JournalService::createEntry([
'entry_date' => substr((string) ($maintenance['maintenance_date'] ?? date('Y-m-d')), 0, 10),
'description_ar' => $desc,
'reference_type' => 'asset_maintenance',
'reference_id' => $maintenanceId,
'source_module' => 'inventory',
'branch_id' => $maintenance['branch_id'] ?? null,
'is_auto_generated' => 1,
], [
['account_id' => $expenseAccount, 'debit' => $cost, 'credit' => '0.00', 'description_ar' => 'مصروف صيانة — ' . $name],
['account_id' => $creditAccount, 'debit' => '0.00', 'credit' => $cost, 'description_ar' => 'سداد/التزام صيانة — ' . $name],
], true);
if (empty($result['success'])) {
Logger::error('Asset maintenance entry failed', ['maintenance_id' => $maintenanceId, 'error' => $result['error'] ?? null]);
}
}
/**
* Buying a fixed asset is not an expense. The club has exchanged one asset
* for another, so the cost is capitalised and released to the income
......
......@@ -523,6 +523,7 @@ $opPostings = [
'inventory.audit_completed' => 'onStockAuditApproved',
'inventory.asset_acquired' => 'onAssetAcquired',
'inventory.asset_disposed' => 'onAssetDisposed',
'inventory.asset_maintenance_recorded' => 'onAssetMaintenanceRecorded',
'fine.waived' => 'onFineWaived',
];
......
......@@ -157,9 +157,10 @@ class AssetCategoryController extends Controller
return ['error' => 'حساب الأصل مطلوب — من غيره الأصل مش هيتقيّد'];
}
$accum = (int) $request->post('depreciation_account_id', 0);
$expense = (int) $request->post('expense_account_id', 0);
$life = (int) $request->post('default_useful_life_months', 60);
$accum = (int) $request->post('depreciation_account_id', 0);
$expense = (int) $request->post('expense_account_id', 0);
$maintenance = (int) $request->post('maintenance_account_id', 0);
$life = (int) $request->post('default_useful_life_months', 60);
// Either both depreciation accounts or neither. One alone cannot post a
// balanced entry, and a category that silently skips is how the balance
......@@ -181,7 +182,7 @@ class AssetCategoryController extends Controller
// Every account must exist and be postable, whatever the form sent.
$db = App::getInstance()->db();
foreach (['حساب الأصل' => $assetAccount, 'مجمع الإهلاك' => $accum, 'مصروف الإهلاك' => $expense] as $label => $accId) {
foreach (['حساب الأصل' => $assetAccount, 'مجمع الإهلاك' => $accum, 'مصروف الإهلاك' => $expense, 'مصروف الصيانة' => $maintenance] as $label => $accId) {
if ($accId <= 0) {
continue;
}
......@@ -210,6 +211,7 @@ class AssetCategoryController extends Controller
'asset_account_id' => $assetAccount,
'depreciation_account_id' => $accum > 0 ? $accum : null,
'expense_account_id' => $expense > 0 ? $expense : null,
'maintenance_account_id' => $maintenance > 0 ? $maintenance : null,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Inventory\Models\AssetMaintenance;
use App\Modules\Inventory\Models\AssetRegister;
class AssetMaintenanceController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('asset.view');
return $this->view('Inventory.Views.assets.maintenance_index', [
'upcoming' => AssetMaintenance::getUpcoming(30),
]);
}
public function forAsset(Request $request, string $assetId): Response
{
$this->authorize('asset.view');
$asset = App::getInstance()->db()->selectOne(
"SELECT id, asset_tag, asset_name FROM asset_register WHERE id = ?",
[(int) $assetId]
);
if (!$asset) {
return $this->redirect('/inventory/assets')->withError('الأصل غير موجود');
}
return $this->view('Inventory.Views.assets.maintenance_form', [
'asset' => $asset,
'history' => AssetMaintenance::getForAsset((int) $assetId),
'types' => AssetMaintenance::getTypes(),
'suppliers' => App::getInstance()->db()->select(
"SELECT id, name_ar FROM suppliers WHERE is_active = 1 ORDER BY name_ar"
),
]);
}
public function store(Request $request, string $assetId): Response
{
$this->authorize('asset.manage');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$asset = $db->selectOne("SELECT id FROM asset_register WHERE id = ?", [(int) $assetId]);
if (!$asset) {
return $this->redirect('/inventory/assets')->withError('الأصل غير موجود');
}
$date = trim((string) $request->post('maintenance_date', ''));
$description = trim((string) $request->post('description', ''));
$cost = trim((string) $request->post('cost', '0'));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return $this->redirect('/inventory/assets/' . $assetId . '/maintenance')->withError('تاريخ الصيانة مطلوب بصيغة صحيحة');
}
if ($description === '') {
return $this->redirect('/inventory/assets/' . $assetId . '/maintenance')->withError('وصف أعمال الصيانة مطلوب');
}
if (!is_numeric($cost) || bccomp($cost, '0.00', 2) < 0) {
return $this->redirect('/inventory/assets/' . $assetId . '/maintenance')->withError('تكلفة الصيانة غير صحيحة');
}
$type = (string) $request->post('maintenance_type', 'corrective');
$paymentMethod = (string) $request->post('payment_method', 'cash');
$nextDue = trim((string) $request->post('next_due_date', ''));
$maintenanceId = (int) $db->insert('asset_maintenance', [
'asset_id' => (int) $assetId,
'maintenance_date' => $date,
'maintenance_type' => \in_array($type, ['preventive', 'corrective', 'emergency'], true) ? $type : 'corrective',
'description' => $description,
'cost' => number_format((float) $cost, 2, '.', ''),
'vendor_name' => trim((string) $request->post('vendor_name', '')) ?: null,
'supplier_id' => ((int) $request->post('supplier_id', 0)) ?: null,
'payment_method' => \in_array($paymentMethod, ['cash', 'bank', 'payable'], true) ? $paymentMethod : 'cash',
'next_due_date' => preg_match('/^\d{4}-\d{2}-\d{2}$/', $nextDue) ? $nextDue : null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// A maintenance cost of zero (e.g. a warranty repair) is still worth
// logging for the asset's history, but there is nothing to post.
if (bccomp((string) $cost, '0.00', 2) > 0) {
EventBus::dispatch('inventory.asset_maintenance_recorded', [
'maintenance_id' => $maintenanceId,
'asset_id' => (int) $assetId,
'cost' => number_format((float) $cost, 2, '.', ''),
'payment_method' => $paymentMethod,
'maintenance_date' => $date,
]);
}
return $this->redirect('/inventory/assets/' . $assetId . '/maintenance')->withSuccess('تم تسجيل عملية الصيانة');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class AssetMaintenance extends Model
{
protected static string $table = 'asset_maintenance';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'asset_id',
'maintenance_date',
'maintenance_type',
'description',
'cost',
'vendor_name',
'supplier_id',
'payment_method',
'next_due_date',
'notes',
'created_by',
];
public static function getForAsset(int $assetId): array
{
return App::getInstance()->db()->select(
"SELECT m.*, s.name_ar AS supplier_name
FROM asset_maintenance m
LEFT JOIN suppliers s ON s.id = m.supplier_id
WHERE m.asset_id = ?
ORDER BY m.maintenance_date DESC, m.id DESC",
[$assetId]
);
}
public static function getTypes(): array
{
return [
'preventive' => 'صيانة دورية',
'corrective' => 'صيانة تصحيحية',
'emergency' => 'صيانة طارئة',
];
}
public static function getUpcoming(int $daysAhead = 30): array
{
return App::getInstance()->db()->select(
"SELECT m.*, a.asset_tag, a.asset_name
FROM asset_maintenance m
JOIN asset_register a ON a.id = m.asset_id
WHERE m.next_due_date IS NOT NULL
AND m.next_due_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
ORDER BY m.next_due_date ASC",
[$daysAhead]
);
}
}
......@@ -76,6 +76,9 @@ return [
['GET', '/inventory/assets/{id:\d+}', 'Inventory\Controllers\AssetController@show', ['auth'], 'asset.view'],
['POST', '/inventory/assets/{id:\d+}/dispose', 'Inventory\Controllers\AssetController@dispose', ['auth', 'csrf'], 'asset.manage'],
['POST', '/inventory/assets/run-depreciation', 'Inventory\Controllers\AssetController@runDepreciation',['auth', 'csrf'], 'asset.manage'],
['GET', '/inventory/assets/maintenance', 'Inventory\Controllers\AssetMaintenanceController@index', ['auth'], 'asset.view'],
['GET', '/inventory/assets/{id:\d+}/maintenance', 'Inventory\Controllers\AssetMaintenanceController@forAsset',['auth'], 'asset.view'],
['POST', '/inventory/assets/{id:\d+}/maintenance', 'Inventory\Controllers\AssetMaintenanceController@store', ['auth', 'csrf'], 'asset.manage'],
// Fixed-asset categories — the GL mapping depreciation posts through
['GET', '/inventory/asset-categories', 'Inventory\Controllers\AssetCategoryController@index', ['auth'], 'asset.view'],
......
......@@ -99,6 +99,11 @@ $accountSelect = static function (string $name, $selected, array $accounts, bool
<strong>٣٣١٦xx</strong> للعمومية والإدارية
</small>
</div>
<div class="form-group">
<label class="form-label">مصروف الصيانة</label>
<?php $accountSelect('maintenance_account_id', $category['maintenance_account_id'] ?? 0, $accounts); ?>
<small style="color:#6B7280;">الحساب اللي بتتقيّد عليه تكلفة صيانة أصول هذه الفئة — بدون تحديده لن تُرحّل عمليات الصيانة للدفتر العام</small>
</div>
</div>
<div style="margin-top:14px;padding:12px 14px;background:#FFFBEB;border-radius:6px;color:#92400E;font-size:12.5px;line-height:1.9;">
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>صيانة أصل <?= e($asset['asset_tag']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/assets/<?= (int) $asset['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> بيانات الأصل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (can('asset.manage')): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="wrench" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تسجيل عملية صيانة — <?= e($asset['asset_name'] ?: $asset['asset_tag']) ?></h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/inventory/assets/<?= (int) $asset['id'] ?>/maintenance">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">تاريخ الصيانة <span style="color:#DC2626;">*</span></label>
<input type="date" name="maintenance_date" class="form-input" value="<?= e(date('Y-m-d')) ?>" required>
</div>
<div class="form-group">
<label class="form-label">نوع الصيانة</label>
<select name="maintenance_type" class="form-select">
<?php foreach ($types as $key => $label): ?>
<option value="<?= e($key) ?>"><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">التكلفة</label>
<input type="number" name="cost" class="form-input" step="0.01" min="0" value="0.00" style="direction:ltr;text-align:left;">
</div>
</div>
<div class="form-group">
<label class="form-label">وصف أعمال الصيانة <span style="color:#DC2626;">*</span></label>
<textarea name="description" class="form-input" rows="2" required placeholder="ما تم فعله..."></textarea>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">المورد / الفني</label>
<select name="supplier_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= (int) $s['id'] ?>"><?= e($s['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">اسم المورد (إن لم يكن مسجلًا)</label>
<input type="text" name="vendor_name" class="form-input">
</div>
<div class="form-group">
<label class="form-label">طريقة السداد</label>
<select name="payment_method" class="form-select">
<option value="cash">نقدي</option>
<option value="bank">تحويل بنكي</option>
<option value="payable">آجل (على الحساب)</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:15px;">
<div class="form-group">
<label class="form-label">تاريخ الصيانة القادمة (اختياري)</label>
<input type="date" name="next_due_date" class="form-input">
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" class="form-input">
</div>
</div>
<button type="submit" class="btn btn-primary">حفظ عملية الصيانة</button>
</form>
</div>
</div>
<?php endif; ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="history" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">سجل الصيانة</h3>
</div>
<?php if (!empty($history)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>النوع</th>
<th>الوصف</th>
<th>المورد</th>
<th>التكلفة</th>
<th>الصيانة القادمة</th>
</tr>
</thead>
<tbody>
<?php foreach ($history as $m): ?>
<tr>
<td style="white-space:nowrap;"><?= e($m['maintenance_date']) ?></td>
<td><?= e($types[$m['maintenance_type']] ?? $m['maintenance_type']) ?></td>
<td><?= e($m['description']) ?></td>
<td><?= e($m['supplier_name'] ?? $m['vendor_name'] ?? '—') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($m['cost']) ?></td>
<td><?= e($m['next_due_date'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="wrench" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا يوجد سجل صيانة لهذا الأصل بعد</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>صيانة الأصول<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/assets" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> قائمة الأصول</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="calendar-clock" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">صيانة مستحقة خلال 30 يوم</h3>
</div>
<?php if (!empty($upcoming)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الأصل</th>
<th>آخر صيانة</th>
<th>موعد الصيانة القادمة</th>
</tr>
</thead>
<tbody>
<?php foreach ($upcoming as $u): ?>
<tr>
<td>
<a href="/inventory/assets/<?= (int) $u['asset_id'] ?>/maintenance">
<?= e($u['asset_name'] ?: $u['asset_tag']) ?>
</a>
</td>
<td><?= e($u['maintenance_date']) ?></td>
<td style="font-weight:600;color:#D97706;"><?= e($u['next_due_date']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="check-circle" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد صيانة مستحقة خلال الشهر القادم</p>
<p style="margin:5px 0 0;font-size:13px;">افتح صفحة أي أصل من قائمة الأصول لتسجيل عملية صيانة له</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
});
</script>
<?php $__template->endSection(); ?>
......@@ -7,6 +7,9 @@
<i data-lucide="pencil" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل الأصل
</a>
<?php endif; ?>
<a href="/inventory/assets/<?= (int) $asset['id'] ?>/maintenance" class="btn btn-outline">
<i data-lucide="wrench" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الصيانة
</a>
<a href="/inventory/assets" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
......
......@@ -55,6 +55,7 @@ MenuRegistry::register('inventory', [
['label_ar' => 'الأصول والإهلاك', 'label_en' => 'Assets', 'route' => '/inventory/assets', 'permission' => 'asset.view', 'order' => 9],
['label_ar' => 'فئات الأصول', 'label_en' => 'Asset Categories', 'route' => '/inventory/asset-categories', 'permission' => 'asset.view', 'order' => 10],
['label_ar' => 'عهدة الأصول', 'label_en' => 'Asset Custody', 'route' => '/inventory/assets/custody', 'permission' => 'asset.view', 'order' => 10],
['label_ar' => 'صيانة الأصول', 'label_en' => 'Asset Maintenance', 'route' => '/inventory/assets/maintenance', 'permission' => 'asset.view', 'order' => 10],
['label_ar' => 'قوائم المواد (BOM)', 'label_en' => 'Bill of Materials', 'route' => '/inventory/bom', 'permission' => 'inventory.bom.view','order' => 11],
['label_ar' => 'أرصدة افتتاحية', 'label_en' => 'Opening Balances', 'route' => '/inventory/opening-balances', 'permission' => 'inventory.opening.manage', 'order' => 12],
['label_ar' => 'تقارير المخزون', 'label_en' => 'Inventory Reports', 'route' => '/inventory/reports/stock-balance','permission' => 'report.inventory', 'order' => 13],
......
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `asset_maintenance` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`asset_id` BIGINT UNSIGNED NOT NULL,
`maintenance_date` DATE NOT NULL,
`maintenance_type` ENUM('preventive','corrective','emergency') NOT NULL DEFAULT 'corrective',
`description` TEXT NOT NULL,
`cost` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`vendor_name` VARCHAR(200) NULL,
`supplier_id` BIGINT UNSIGNED NULL,
`payment_method` ENUM('cash','bank','payable') NOT NULL DEFAULT 'cash',
`next_due_date` DATE NULL,
`notes` TEXT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_asset_maintenance_asset` (`asset_id`),
INDEX `idx_asset_maintenance_date` (`maintenance_date`),
CONSTRAINT `fk_asset_maintenance_asset` FOREIGN KEY (`asset_id`) REFERENCES `asset_register`(`id`),
CONSTRAINT `fk_asset_maintenance_supplier` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `asset_categories`
ADD COLUMN `maintenance_account_id` INT UNSIGNED NULL AFTER `expense_account_id`;
",
'down' => "
ALTER TABLE `asset_categories` DROP COLUMN `maintenance_account_id`;
DROP TABLE IF EXISTS `asset_maintenance`;
",
];
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment