From 5b524399d99e220e469029675c5f4b98db81674e Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:07:51 +0100 Subject: [PATCH 01/36] Use RMB for asset audit API Signed-off-by: snipe --- app/Http/Controllers/Api/AssetsController.php | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index d780bd8bd..c20568ed7 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -1064,7 +1064,7 @@ class AssetsController extends Controller * @param int $id * @since [v4.0] */ - public function audit(Request $request): JsonResponse + public function audit(Request $request, Asset $asset): JsonResponse { $this->authorize('audit', Asset::class); @@ -1072,20 +1072,15 @@ class AssetsController extends Controller $settings = Setting::getSettings(); $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); - // No tag passed - return an error - if (!$request->filled('asset_tag')) { - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => '', - 'error' => trans('admin/hardware/message.no_tag'), - ], trans('admin/hardware/message.no_tag')), 200); + // Allow the asset tag to be passed in the payload (legacy method) + if ($request->filled('asset_tag')) { + $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first(); } - - $asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first(); - - if ($asset) { + $originalValues = $asset->getRawOriginal(); + /** * Even though we do a save() further down, we don't want to log this as a "normal" asset update, * which would trigger the Asset Observer and would log an asset *update* log entry (because the @@ -1116,18 +1111,52 @@ class AssetsController extends Controller $asset->last_audit_date = date('Y-m-d H:i:s'); + // Set up the payload for re-display in the API response + $payload = [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => $request->input('note'), + 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date), + ]; + + + /** + * Update custom fields in the database. + * Validation for these fields is handled through the AssetRequest form request + * $model = AssetModel::find($request->get('model_id')); + */ + + if (($asset->model) && ($asset->model->fieldset)) { + $payload['custom_fields'] = []; + foreach ($asset->model->fieldset->fields as $field) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } + } + } else { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } + } + $payload['custom_fields'][$field->db_column] = $request->input($field->db_column); + } + } + + + /** * Invoke Watson Validating to check the asset itself and check to make sure it saved correctly. * We have to invoke this manually because of the unsetEventDispatcher() above.) */ if ($asset->isValid() && $asset->save()) { - $asset->logAudit(request('note'), request('location_id')); - - return response()->json(Helper::formatStandardApiResponse('success', [ - 'asset_tag' => e($asset->asset_tag), - 'note' => e($request->input('note')), - 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date), - ], trans('admin/hardware/message.audit.success'))); + $asset->logAudit(request('note'), request('location_id'), null, $originalValues); + return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success'))); } // Asset failed validation or was not able to be saved From cfa8ddffc091547d3982c5d11f2bc2a53491d5b4 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:08:05 +0100 Subject: [PATCH 02/36] Keep legacy URL for audit Signed-off-by: snipe --- routes/api.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/routes/api.php b/routes/api.php index 9adc83af2..5724990c7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -511,8 +511,17 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ->where(['action' => 'audit|audits|checkins', 'upcoming_status' => 'due|overdue|due-or-overdue']); + // Legacy URL for audit + Route::post('audit', + [ + Api\AssetsController::class, + 'audit' + ] + )->name('api.asset.audit.legacy'); - Route::post('audit', + + // Newer url for audit + Route::post('{asset}/audit', [ Api\AssetsController::class, 'audit' From 6a1bb06c1365164dd0b085fe767f9fe436e45e56 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:09:14 +0100 Subject: [PATCH 03/36] Added tests Signed-off-by: snipe --- tests/Feature/Assets/Api/AuditAssetTest.php | 78 +++++++++++++++++++++ tests/Feature/Assets/Ui/AuditAssetTest.php | 35 +++++++++ 2 files changed, 113 insertions(+) create mode 100644 tests/Feature/Assets/Api/AuditAssetTest.php create mode 100644 tests/Feature/Assets/Ui/AuditAssetTest.php diff --git a/tests/Feature/Assets/Api/AuditAssetTest.php b/tests/Feature/Assets/Api/AuditAssetTest.php new file mode 100644 index 000000000..f93a65ae6 --- /dev/null +++ b/tests/Feature/Assets/Api/AuditAssetTest.php @@ -0,0 +1,78 @@ +actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit', 123456789)) + ->assertStatusMessageIs('error'); + } + + public function testRequiresPermissionToAuditAsset() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->create()) + ->postJson(route('api.asset.audit', $asset)) + ->assertForbidden(); + } + + public function testLegacyAssetAuditIsSaved() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit.legacy'), [ + 'asset_tag' => $asset->asset_tag, + 'note' => 'test', + ]) + ->assertStatusMessageIs('success') + ->assertJson( + [ + 'messages' =>trans('admin/hardware/message.audit.success'), + 'payload' => [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => 'test' + ], + ]) + ->assertStatus(200); + + } + + + public function testAssetAuditIsSaved() + { + $asset = Asset::factory()->create(); + $this->actingAsForApi(User::factory()->auditAssets()->create()) + ->postJson(route('api.asset.audit', $asset), [ + 'note' => 'test' + ]) + ->assertStatusMessageIs('success') + ->assertJson( + [ + 'messages' =>trans('admin/hardware/message.audit.success'), + 'payload' => [ + 'id' => $asset->id, + 'asset_tag' => $asset->asset_tag, + 'note' => 'test' + ], + ]) + ->assertStatus(200); + + } + + +} diff --git a/tests/Feature/Assets/Ui/AuditAssetTest.php b/tests/Feature/Assets/Ui/AuditAssetTest.php new file mode 100644 index 000000000..8b7f49ffc --- /dev/null +++ b/tests/Feature/Assets/Ui/AuditAssetTest.php @@ -0,0 +1,35 @@ +create(); + $this->actingAs(User::factory()->create()) + ->get(route('clone/hardware', $asset)) + ->assertForbidden(); + } + + public function testPageCanBeAccessed(): void + { + $asset = Asset::factory()->create(); + $response = $this->actingAs(User::factory()->auditAssets()->create()) + ->get(route('asset.audit.create', $asset)); + $response->assertStatus(200); + } + + public function testAssetCanBeAudited() + { + $asset = Asset::factory()->create(['name'=>'Asset to clone']); + $this->actingAs(User::factory()->auditAssets()->create()) + ->post(route('asset.audit.store', $asset)) + ->assertStatus(302) + ->assertRedirect(route('assets.audit.due')); + } +} From dfdc24936d8813b5bba492350a7451a1e2855248 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:09:29 +0100 Subject: [PATCH 04/36] Added custom fields partial Signed-off-by: snipe --- resources/views/hardware/audit.blade.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/views/hardware/audit.blade.php b/resources/views/hardware/audit.blade.php index baeb56d59..72be0e833 100644 --- a/resources/views/hardware/audit.blade.php +++ b/resources/views/hardware/audit.blade.php @@ -115,6 +115,12 @@ + + @include("models/custom_fields_form", [ + 'model' => $asset->model, + 'show_display_checkout_fields' => 'true' + ]) +
From b8a9db2faf3db2f8eedf39e3791e461911dc1fed Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:09:51 +0100 Subject: [PATCH 05/36] Added display_audit to custom fields list Signed-off-by: snipe --- resources/views/custom_fields/index.blade.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/views/custom_fields/index.blade.php b/resources/views/custom_fields/index.blade.php index 5b9338baf..ee2778d77 100644 --- a/resources/views/custom_fields/index.blade.php +++ b/resources/views/custom_fields/index.blade.php @@ -196,6 +196,14 @@ + + + + {{ trans('admin/custom_fields/general.display_audit') }} + + + {{ trans('admin/custom_fields/general.field_element_short') }} @@ -227,6 +235,7 @@ {!! ($field->is_unique=='1') ? '' : '' !!} {!! ($field->display_checkin=='1') ? '' : '' !!} {!! ($field->display_checkout=='1') ? '' : '' !!} + {!! ($field->display_audit=='1') ? '' : '' !!} {{ $field->element }} @foreach($field->fieldset as $fieldset) From acdbf452e238cbe0e3f4c43b79c7c12d87892c89 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:10:01 +0100 Subject: [PATCH 06/36] Added checkbox to custom field form Signed-off-by: snipe --- resources/views/custom_fields/fields/edit.blade.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/views/custom_fields/fields/edit.blade.php b/resources/views/custom_fields/fields/edit.blade.php index 8ff41b7a4..4969899a9 100644 --- a/resources/views/custom_fields/fields/edit.blade.php +++ b/resources/views/custom_fields/fields/edit.blade.php @@ -224,6 +224,14 @@
+ +
+ +
+
From 241777c1fd23e3c58b152c10380679627fb3a310 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:10:12 +0100 Subject: [PATCH 07/36] Added translation string Signed-off-by: snipe --- resources/lang/en-US/admin/custom_fields/general.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en-US/admin/custom_fields/general.php b/resources/lang/en-US/admin/custom_fields/general.php index f17eedda1..c9ae61c96 100644 --- a/resources/lang/en-US/admin/custom_fields/general.php +++ b/resources/lang/en-US/admin/custom_fields/general.php @@ -59,5 +59,6 @@ return [ 'encrypted_options' => 'This field is encrypted, so some display options will not be available.', 'display_checkin' => 'Display in checkin forms', 'display_checkout' => 'Display in checkout forms', + 'display_audit' => 'Display in audit forms', ]; From 95fef9682ff79657b414de703c2bd494f7f5303c Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:10:18 +0100 Subject: [PATCH 08/36] Added migration Signed-off-by: snipe --- ...4_07_145418_add_custom_fields_to_audit.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php diff --git a/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php b/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php new file mode 100644 index 000000000..d5594fc03 --- /dev/null +++ b/database/migrations/2025_04_07_145418_add_custom_fields_to_audit.php @@ -0,0 +1,28 @@ +boolean('display_audit')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('custom_fields', function (Blueprint $table) { + $table->dropColumn('display_audit'); + }); + } +}; From c344c4031070837710fd43d2afa819fe60f96009 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:10:36 +0100 Subject: [PATCH 09/36] Added `auditAssets()` to user factory Signed-off-by: snipe --- database/factories/UserFactory.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index bb89d3524..d8b1ac141 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -385,6 +385,12 @@ class UserFactory extends Factory return $this->appendPermission(['suppliers.delete' => '1']); } + public function auditAssets() + { + return $this->appendPermission(['assets.audit' => '1']); + } + + private function appendPermission(array $permission) { return $this->state(function ($currentState) use ($permission) { From ce460c9ab0c50d9f5270302bff749d50c39a6cb4 Mon Sep 17 00:00:00 2001 From: snipe Date: Mon, 7 Apr 2025 22:15:09 +0100 Subject: [PATCH 10/36] Updated route Signed-off-by: snipe --- resources/views/hardware/quickscan.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/hardware/quickscan.blade.php b/resources/views/hardware/quickscan.blade.php index e81981ed6..0dd3fb570 100644 --- a/resources/views/hardware/quickscan.blade.php +++ b/resources/views/hardware/quickscan.blade.php @@ -142,7 +142,7 @@ var formData = $('#audit-form').serializeArray(); $.ajax({ - url: "{{ route('api.asset.audit') }}", + url: "{{ route('api.asset.audit.legacy') }}", type : 'POST', headers: { "X-Requested-With": 'XMLHttpRequest', From 226ad52f0739128f2e6f452b7ce6ab493289d424 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 01:59:08 +0100 Subject: [PATCH 11/36] Better UI route Signed-off-by: snipe --- routes/web/hardware.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/web/hardware.php b/routes/web/hardware.php index 3b65f730e..21f4f40d4 100644 --- a/routes/web/hardware.php +++ b/routes/web/hardware.php @@ -63,14 +63,14 @@ Route::group( ->push(trans_choice('general.checkin_due_days', Setting::getSettings()->due_checkin_days, ['days' => Setting::getSettings()->due_checkin_days]), route('assets.audit.due')) ); - Route::get('audit/{asset}', [AssetsController::class, 'audit']) + Route::get('{asset}/audit', [AssetsController::class, 'audit']) ->name('asset.audit.create') ->breadcrumbs(fn (Trail $trail, Asset $asset) => $trail->parent('hardware.show', $asset) ->push(trans('general.audit')) ); - Route::post('audit/{asset}', + Route::post('{asset}/audit', [AssetsController::class, 'auditStore'] )->name('asset.audit.store'); From 362f14a01d044ad60c13c7e89e4016b566269adc Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 01:59:33 +0100 Subject: [PATCH 12/36] Manually invoke a validator Signed-off-by: snipe --- app/Http/Controllers/Api/AssetsController.php | 85 ++++++++------- .../Controllers/Assets/AssetsController.php | 103 +++++++++++------- 2 files changed, 111 insertions(+), 77 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index c20568ed7..d60ded391 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -34,6 +34,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; use App\View\Label; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Validator; /** @@ -1081,22 +1082,6 @@ class AssetsController extends Controller $originalValues = $asset->getRawOriginal(); - /** - * Even though we do a save() further down, we don't want to log this as a "normal" asset update, - * which would trigger the Asset Observer and would log an asset *update* log entry (because the - * de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to - * the audit log entry we're creating through this controller. - * - * To prevent this double-logging (one for update and one for audit), we skip the observer and bypass - * that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher() - * will bypass normal model-level validation that's usually handled at the observer ) - * - * We handle validation on the save() by checking if the asset is valid via the ->isValid() method, - * which manually invokes Watson Validating to make sure the asset's model is valid. - * - * @see \App\Observers\AssetObserver::updating() - */ - $asset->unsetEventDispatcher(); $asset->next_audit_date = $dt; if ($request->filled('next_audit_date')) { @@ -1125,29 +1110,58 @@ class AssetsController extends Controller * Validation for these fields is handled through the AssetRequest form request * $model = AssetModel::find($request->get('model_id')); */ - if (($asset->model) && ($asset->model->fieldset)) { $payload['custom_fields'] = []; foreach ($asset->model->fieldset->fields as $field) { - if ($field->field_encrypted == '1') { - if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (($field->display_audit=='1') && ($request->has($field->db_column))) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } + } + } else { if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); } else { - $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + $asset->{$field->db_column} = $request->input($field->db_column); } } - } else { - if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); - } else { - $asset->{$field->db_column} = $request->input($field->db_column); - } + $payload['custom_fields'][$field->db_column] = $request->input($field->db_column); } - $payload['custom_fields'][$field->db_column] = $request->input($field->db_column); - } - } + } + } + + // Validate custom fields + Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); + + // Validate the rest of the data before we turn off the event dispatcher + if ($asset->isInvalid()) { + return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors())); + } + + + /** + * Even though we do a save() further down, we don't want to log this as a "normal" asset update, + * which would trigger the Asset Observer and would log an asset *update* log entry (because the + * de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to + * the audit log entry we're creating through this controller. + * + * To prevent this double-logging (one for update and one for audit), we skip the observer and bypass + * that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher() + * will bypass normal model-level validation that's usually handled at the observer) + * + * We handle validation on the save() by checking if the asset is valid via the ->isValid() method, + * which manually invokes Watson Validating to make sure the asset's model is valid. + * + * @see \App\Observers\AssetObserver::updating() + * @see \App\Models\Asset::save() + */ + + $asset->unsetEventDispatcher(); /** @@ -1159,19 +1173,12 @@ class AssetsController extends Controller return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success'))); } - // Asset failed validation or was not able to be saved - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => e($asset->asset_tag), - 'error' => $asset->getErrors()->first(), - ], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200); } // No matching asset for the asset tag that was passed. - return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag' => e($request->input('asset_tag')), - 'error' => trans('admin/hardware/message.audit.error'), - ], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200); + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); + } diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 9578acea6..23b438707 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn; use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\ImageUploadRequest; +use App\Http\Requests\UpdateAssetRequest; use App\Models\Actionlog; use App\Http\Requests\UploadFileRequest; use Illuminate\Support\Facades\Log; @@ -390,26 +391,26 @@ class AssetsController extends Controller $asset = $request->handleImages($asset); // Update custom fields in the database. - // Validation for these fields is handlded through the AssetRequest form request // FIXME: No idea why this is returning a Builder error on db_column_name. // Need to investigate and fix. Using static method for now. $model = AssetModel::find($request->get('model_id')); if (($model) && ($model->fieldset)) { foreach ($model->fieldset->fields as $field) { - - if ($field->field_encrypted == '1') { - if (Gate::allows('assets.view.encrypted_custom_fields')) { - if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); - } else { - $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + if ($request->has($field->db_column)) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } } - } - } else { - if (is_array($request->input($field->db_column))) { - $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); } else { - $asset->{$field->db_column} = $request->input($field->db_column); + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } } } } @@ -865,13 +866,6 @@ class AssetsController extends Controller return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList()); } - public function audit(Asset $asset) - { - $settings = Setting::getSettings(); - $this->authorize('audit', Asset::class); - $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); - return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt)->with('locations_list'); - } public function dueForAudit() { @@ -888,19 +882,59 @@ class AssetsController extends Controller } - public function auditStore(UploadFileRequest $request, Asset $asset) + public function audit(Asset $asset) { + $settings = Setting::getSettings(); + $this->authorize('audit', Asset::class); + $dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString(); + return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list'); + } + + public function auditStore(UpdateAssetRequest $request, Asset $asset) + { + $this->authorize('audit', Asset::class); - $rules = [ - 'location_id' => 'exists:locations,id|nullable|numeric', - 'next_audit_date' => 'date|nullable', - ]; + $originalValues = $asset->getRawOriginal(); - $validator = Validator::make($request->all(), $rules); + $asset->next_audit_date = $request->input('next_audit_date'); + $asset->last_audit_date = date('Y-m-d H:i:s'); - if ($validator->fails()) { - return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all())); + // Check to see if they checked the box to update the physical location, + // not just note it in the audit notes + if ($request->input('update_location') == '1') { + $asset->location_id = $request->input('location_id'); + } + + // Update custom fields in the database + if (($asset->model) && ($asset->model->fieldset)) { + foreach ($asset->model->fieldset->fields as $field) { + if (($field->display_audit=='1') && ($request->has($field->db_column))) { + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column))); + } else { + $asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column)); + } + } + } else { + if (is_array($request->input($field->db_column))) { + $asset->{$field->db_column} = implode(', ', $request->input($field->db_column)); + } else { + $asset->{$field->db_column} = $request->input($field->db_column); + } + } + } + } + } + + // Validate custom fields + Validator::make($asset->toArray(), $asset->customFieldValidationRules())->validate(); + + // Validate the rest of the data before we turn off the event dispatcher + if ($asset->isInvalid()) { + return redirect()->back()->withInput()->withErrors($asset->getErrors()); } /** @@ -917,18 +951,11 @@ class AssetsController extends Controller * which manually invokes Watson Validating to make sure the asset's model is valid. * * @see \App\Observers\AssetObserver::updating() + * @see \App\Models\Asset::save() */ + $asset->unsetEventDispatcher(); - $asset->next_audit_date = $request->input('next_audit_date'); - $asset->last_audit_date = date('Y-m-d H:i:s'); - - // Check to see if they checked the box to update the physical location, - // not just note it in the audit notes - if ($request->input('update_location') == '1') { - $asset->location_id = $request->input('location_id'); - } - /** * Invoke Watson Validating to check the asset itself and check to make sure it saved correctly. @@ -942,7 +969,7 @@ class AssetsController extends Controller $file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image')); } - $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name); + $asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues); return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success')); } From 9bb349d34be0933bdbcc4c3db7df347e21197e3a Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:00:37 +0100 Subject: [PATCH 13/36] Try to get the asset from the route if there is RMB Signed-off-by: snipe --- .../Traits/MayContainCustomFields.php | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index bbdf62893..ed0c96dac 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -10,19 +10,28 @@ trait MayContainCustomFields // this gets called automatically on a form request public function withValidator($validator) { - // find the model - if ($this->method() == 'POST') { - $asset_model = AssetModel::find($this->model_id); - } - if ($this->method() == 'PATCH' || $this->method() == 'PUT') { - $asset_model = $this->asset->model; - } + + + // For auditing and some other non-standard things where $this is only the form submission and may not have the asset info + if ((request()->route('asset') && (request()->route('asset')->model_id))) { + $asset_model = AssetModel::find(request()->route('asset')->model_id); += } else { + // find the model + if ($this->method() == 'POST') { + $asset_model = AssetModel::find($this->model_id); + } + if ($this->method() == 'PATCH' || $this->method() == 'PUT') { + $asset_model = $this->asset->model; + } + } + // collect the custom fields in the request $validator->after(function ($validator) use ($asset_model) { $request_fields = $this->collect()->keys()->filter(function ($attributes) { return str_starts_with($attributes, '_snipeit_'); }); - // if there are custom fields, find the one's that don't exist on the model's fieldset and add an error to the validator's error bag + + // if there are custom fields, find the ones that don't exist on the model's fieldset and add an error to the validator's error bag if (count($request_fields) > 0 && $validator->errors()->isEmpty()) { $request_fields->diff($asset_model?->fieldset?->fields?->pluck('db_column')) ->each(function ($request_field_name) use ($request_fields, $validator) { From f37ed3e0552ed3f04cd9acdfb2f1e85496209d08 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:00:51 +0100 Subject: [PATCH 14/36] Add display_audit to custom fields controller Signed-off-by: snipe --- app/Http/Controllers/CustomFieldsController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Controllers/CustomFieldsController.php b/app/Http/Controllers/CustomFieldsController.php index 7c0bdefa2..4c63179d5 100644 --- a/app/Http/Controllers/CustomFieldsController.php +++ b/app/Http/Controllers/CustomFieldsController.php @@ -106,6 +106,7 @@ class CustomFieldsController extends Controller "show_in_requestable_list" => $request->get("show_in_requestable_list", 0), "display_checkin" => $request->get("display_checkin", 0), "display_checkout" => $request->get("display_checkout", 0), + "display_audit" => $request->get("display_audit", 0), "created_by" => auth()->id() ]); @@ -250,6 +251,7 @@ class CustomFieldsController extends Controller $field->show_in_requestable_list = $request->get("show_in_requestable_list", 0); $field->display_checkin = $request->get("display_checkin", 0); $field->display_checkout = $request->get("display_checkout", 0); + $field->display_audit = $request->get("display_audit", 0); if ($request->get('format') == 'CUSTOM REGEX') { $field->format = e($request->get('custom_format')); From f4e3e6ceb674918f025f83a4c21c06199cf00397 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:01:08 +0100 Subject: [PATCH 15/36] Added display_audit to custom fields transformer Signed-off-by: snipe --- app/Http/Transformers/CustomFieldsTransformer.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Transformers/CustomFieldsTransformer.php b/app/Http/Transformers/CustomFieldsTransformer.php index d6401a3e5..501d264b5 100644 --- a/app/Http/Transformers/CustomFieldsTransformer.php +++ b/app/Http/Transformers/CustomFieldsTransformer.php @@ -50,6 +50,9 @@ class CustomFieldsTransformer 'display_in_user_view' => ($field->display_in_user_view =='1') ? true : false, 'auto_add_to_fieldsets' => ($field->auto_add_to_fieldsets == '1') ? true : false, 'show_in_listview' => ($field->show_in_listview == '1') ? true : false, + 'display_checkin' => ($field->display_checkin == '1') ? true : false, + 'display_checkout' => ($field->display_checkout == '1') ? true : false, + 'display_audit' => ($field->display_audit == '1') ? true : false, 'created_at' => Helper::getFormattedDateObject($field->created_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($field->updated_at, 'datetime'), ]; From e4180c21941a73e1582819cad19f80c37a6a3220 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:01:29 +0100 Subject: [PATCH 16/36] Removed duplicated code Signed-off-by: snipe --- app/Models/Asset.php | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 673012cf6..a0e113279 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -213,6 +213,31 @@ class Asset extends Depreciable $this->attributes['expected_checkin'] = $value; } + + + public function customFieldValidationRules() + { + + $customFieldValidationRules = []; + + if (($this->model) && ($this->model->fieldset)) { + + foreach ($this->model->fieldset->fields as $field) { + + if ($field->format == 'BOOLEAN'){ + $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); + } + } + + $customFieldValidationRules += $this->model->fieldset->validation_rules(); + } + + return $customFieldValidationRules; + + } + + + /** * This handles the custom field validation for assets * @@ -220,29 +245,7 @@ class Asset extends Depreciable */ public function save(array $params = []) { - if ($this->model_id != '') { - $model = AssetModel::find($this->model_id); - - if (($model) && ($model->fieldset)) { - - foreach ($model->fieldset->fields as $field){ - if($field->format == 'BOOLEAN'){ - $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); - } - } - - $this->rules += $model->fieldset->validation_rules(); - - if ($this->model->fieldset){ - foreach ($this->model->fieldset->fields as $field){ - if($field->format == 'BOOLEAN'){ - $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); - } - } - } - } - } - + $this->rules += $this->customFieldValidationRules(); return parent::save($params); } @@ -254,7 +257,7 @@ class Asset extends Depreciable /** * Returns the warranty expiration date as Carbon object - * @return \Carbon|null + * @return \Carbon\Carbon|null */ public function getWarrantyExpiresAttribute() { From 5bbba56b0e7a0c29dc9d2ed56f9772fe7fecd6ad Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:01:39 +0100 Subject: [PATCH 17/36] Added orginal values for logging Signed-off-by: snipe --- app/Models/Loggable.php | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/Models/Loggable.php b/app/Models/Loggable.php index 1e4a91202..15157cc6a 100644 --- a/app/Models/Loggable.php +++ b/app/Models/Loggable.php @@ -220,9 +220,41 @@ trait Loggable * @since [v4.0] * @return \App\Models\Actionlog */ - public function logAudit($note, $location_id, $filename = null) + public function logAudit($note, $location_id, $filename = null, $originalValues = []) { + $log = new Actionlog; + + if (static::class == Asset::class) { + if ($asset = Asset::find($log->item_id)) { + // add the custom fields that were changed + if ($asset->model->fieldset) { + $fields_array = []; + foreach ($asset->model->fieldset->fields as $field) { + if ($field->display_audit == 1) { + $fields_array[$field->db_column] = $asset->{$field->db_column}; + } + } + } + } + } + + $changed = []; + + unset($originalValues['updated_at'], $originalValues['last_audit_date']); + foreach ($originalValues as $key => $value) { + + if ($value != $this->getAttributes()[$key]) { + $changed[$key]['old'] = $value; + $changed[$key]['new'] = $this->getAttributes()[$key]; + } + } + + if (!empty($changed)){ + $log->log_meta = json_encode($changed); + } + + $location = Location::find($location_id); if (static::class == LicenseSeat::class) { $log->item_type = License::class; @@ -235,6 +267,7 @@ trait Loggable $log->note = $note; $log->created_by = auth()->id(); $log->filename = $filename; + $log->action_date = date('Y-m-d H:i:s'); $log->logaction('audit'); $params = [ From 62e863a0fa0fe8ad5212483fdac99151abde8294 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:02:10 +0100 Subject: [PATCH 18/36] Removed tooltip code This throws an error currently Signed-off-by: snipe --- resources/views/models/custom_fields_form.blade.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/resources/views/models/custom_fields_form.blade.php b/resources/views/models/custom_fields_form.blade.php index 3297c15c9..2346218a3 100644 --- a/resources/views/models/custom_fields_form.blade.php +++ b/resources/views/models/custom_fields_form.blade.php @@ -101,10 +101,4 @@ @endif - + From 74f8cb52988dc026b8b4742a9bd1d8d6c207898a Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:02:28 +0100 Subject: [PATCH 19/36] Updated url in bootstrap partial Signed-off-by: snipe --- resources/views/partials/bootstrap-table.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/bootstrap-table.blade.php b/resources/views/partials/bootstrap-table.blade.php index 76656a0a2..761acf481 100644 --- a/resources/views/partials/bootstrap-table.blade.php +++ b/resources/views/partials/bootstrap-table.blade.php @@ -341,7 +341,7 @@ function hardwareAuditFormatter(value, row) { - return '{{ trans('general.audit') }}'; + return '{{ trans('general.audit') }}'; } From 4aeba2a96b51745a3bc25002d5c1367192db5238 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:02:37 +0100 Subject: [PATCH 20/36] Try to fix tests Signed-off-by: snipe --- tests/Feature/Assets/Ui/AuditAssetTest.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/Feature/Assets/Ui/AuditAssetTest.php b/tests/Feature/Assets/Ui/AuditAssetTest.php index 8b7f49ffc..c36282366 100644 --- a/tests/Feature/Assets/Ui/AuditAssetTest.php +++ b/tests/Feature/Assets/Ui/AuditAssetTest.php @@ -10,25 +10,22 @@ class AuditAssetTest extends TestCase { public function testPermissionRequiredToCreateAssetModel() { - $asset = Asset::factory()->create(); $this->actingAs(User::factory()->create()) - ->get(route('clone/hardware', $asset)) + ->get(route('clone/hardware', Asset::factory()->create())) ->assertForbidden(); } public function testPageCanBeAccessed(): void { - $asset = Asset::factory()->create(); - $response = $this->actingAs(User::factory()->auditAssets()->create()) - ->get(route('asset.audit.create', $asset)); - $response->assertStatus(200); + $this->actingAs(User::factory()->auditAssets()->create()) + ->get(route('asset.audit.create', Asset::factory()->create())) + ->assertStatus(200); } public function testAssetCanBeAudited() { - $asset = Asset::factory()->create(['name'=>'Asset to clone']); $this->actingAs(User::factory()->auditAssets()->create()) - ->post(route('asset.audit.store', $asset)) + ->post(route('asset.audit.store', Asset::factory()->create())) ->assertStatus(302) ->assertRedirect(route('assets.audit.due')); } From 4b21f0d00b04ce7e41e200361d8c72a7efde8a67 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:03:45 +0100 Subject: [PATCH 21/36] Removed stray character Signed-off-by: snipe --- app/Http/Requests/Traits/MayContainCustomFields.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index ed0c96dac..da079c56d 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -15,7 +15,7 @@ trait MayContainCustomFields // For auditing and some other non-standard things where $this is only the form submission and may not have the asset info if ((request()->route('asset') && (request()->route('asset')->model_id))) { $asset_model = AssetModel::find(request()->route('asset')->model_id); -= } else { + } else { // find the model if ($this->method() == 'POST') { $asset_model = AssetModel::find($this->model_id); @@ -25,6 +25,7 @@ trait MayContainCustomFields } } + // collect the custom fields in the request $validator->after(function ($validator) use ($asset_model) { $request_fields = $this->collect()->keys()->filter(function ($attributes) { From c4d0afb8d4bfde1b65bfe54092d32a191ce7299a Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:24:33 +0100 Subject: [PATCH 22/36] Added comments Signed-off-by: snipe --- .../Traits/MayContainCustomFields.php | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index da079c56d..9fd09d507 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -13,17 +13,26 @@ trait MayContainCustomFields // For auditing and some other non-standard things where $this is only the form submission and may not have the asset info - if ((request()->route('asset') && (request()->route('asset')->model_id))) { - $asset_model = AssetModel::find(request()->route('asset')->model_id); - } else { - // find the model - if ($this->method() == 'POST') { - $asset_model = AssetModel::find($this->model_id); - } - if ($this->method() == 'PATCH' || $this->method() == 'PUT') { - $asset_model = $this->asset->model; - } - } + // In case the model is being changed + if (request()->input('model_id')->model_id!='') { + + $asset_model = AssetModel::find(request()->route('asset')->model_id); + + // or if we have it available to route-model-binding + } elseif ((request()->route('asset') && (request()->route('asset')->model_id))) { + + $asset_model = AssetModel::find(request()->route('asset')->model_id); + + } else { + + if ($this->method() == 'POST') { + $asset_model = AssetModel::find($this->model_id); + } + + if ($this->method() == 'PATCH' || $this->method() == 'PUT') { + $asset_model = $this->asset->model; + } + } // collect the custom fields in the request From 908bb357922bd57a561163095dc99a902d8b74f8 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:38:35 +0100 Subject: [PATCH 23/36] Use upload file request Signed-off-by: snipe --- app/Http/Controllers/Assets/AssetsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 23b438707..835689977 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -890,7 +890,7 @@ class AssetsController extends Controller return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list'); } - public function auditStore(UpdateAssetRequest $request, Asset $asset) + public function auditStore(UploadFileRequest $request, Asset $asset) { $this->authorize('audit', Asset::class); From c59e9770b77016c1e350fb3ac9a519d7f2cc81e6 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:39:05 +0100 Subject: [PATCH 24/36] Removed unusued property Signed-off-by: snipe --- app/Http/Requests/Traits/MayContainCustomFields.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index 9fd09d507..c0b624c33 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -14,7 +14,7 @@ trait MayContainCustomFields // For auditing and some other non-standard things where $this is only the form submission and may not have the asset info // In case the model is being changed - if (request()->input('model_id')->model_id!='') { + if (request()->input('model_id')!='') { $asset_model = AssetModel::find(request()->route('asset')->model_id); From 5beb0bf534e75c676906bb8cd993147366a7b447 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 02:39:17 +0100 Subject: [PATCH 25/36] Added assertion for success in test Signed-off-by: snipe --- tests/Feature/Assets/Ui/AuditAssetTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Assets/Ui/AuditAssetTest.php b/tests/Feature/Assets/Ui/AuditAssetTest.php index c36282366..a966e6f45 100644 --- a/tests/Feature/Assets/Ui/AuditAssetTest.php +++ b/tests/Feature/Assets/Ui/AuditAssetTest.php @@ -24,9 +24,11 @@ class AuditAssetTest extends TestCase public function testAssetCanBeAudited() { - $this->actingAs(User::factory()->auditAssets()->create()) + $response = $this->actingAs(User::factory()->auditAssets()->create()) ->post(route('asset.audit.store', Asset::factory()->create())) ->assertStatus(302) ->assertRedirect(route('assets.audit.due')); + + $this->followRedirects($response)->assertSee('success'); } } From d2c73851975c500f5f397d0340c3f31b6f38e802 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 04:06:54 +0100 Subject: [PATCH 26/36] Updated class for error text Signed-off-by: snipe --- public/css/build/app.css | 3 ++- public/css/build/overrides.css | 3 ++- public/css/dist/all.css | 6 ++++-- public/mix-manifest.json | 6 +++--- resources/assets/less/overrides.less | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/public/css/build/app.css b/public/css/build/app.css index 66da69f4e..060421ac6 100644 --- a/public/css/build/app.css +++ b/public/css/build/app.css @@ -1175,7 +1175,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index a4c6ddb9e..fb5c19221 100644 --- a/public/css/build/overrides.css +++ b/public/css/build/overrides.css @@ -806,7 +806,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/css/dist/all.css b/public/css/dist/all.css index d508c1d25..2708d3e64 100644 --- a/public/css/dist/all.css +++ b/public/css/dist/all.css @@ -22510,7 +22510,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { @@ -24061,7 +24062,8 @@ th.css-history > .th-inner::before { padding: 6px 12px; height: 34px; } -.form-group.has-error label { +.form-group.has-error label, +.form-group.has-error .help-block { color: #a94442; } .select2-container--default .select2-selection--multiple { diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 4da14272a..295126469 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -2,8 +2,8 @@ "/js/build/app.js": "/js/build/app.js?id=607de09b70b83ef82a427e4b36341682", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=06c13e817cc022028b3f4a33c0ca303a", "/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=79aa889a1a6691013be6c342ca7391cd", - "/css/build/overrides.css": "/css/build/overrides.css?id=712a9b236379148e57e34b70c878bff9", - "/css/build/app.css": "/css/build/app.css?id=9e4ea63fed30049dc054a87a0acfaa61", + "/css/build/overrides.css": "/css/build/overrides.css?id=4d62149a0ee9dc139bdf03ff2f83930d", + "/css/build/app.css": "/css/build/app.css?id=d47ce0dc14671bb4e462e111001488e5", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=ea22079836a432d7f46a5d390c445e13", @@ -19,7 +19,7 @@ "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=a82b065847bf3cd5d713c04ee8dc86c6", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=6ea836d8126de101081c49abbdb89417", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb", - "/css/dist/all.css": "/css/dist/all.css?id=0d5289d806efaf15e9d47833843cc986", + "/css/dist/all.css": "/css/dist/all.css?id=7c861c2086473c513fe26c21e3c4d433", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde", diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index 4640017c0..8c8b80e46 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -886,7 +886,7 @@ th.css-history > .th-inner::before { height: 34px; } -.form-group.has-error label { +.form-group.has-error label, .form-group.has-error .help-block { color: #a94442; } From e95d7076b922512f7d7265c3b0cde808146b63c8 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 04:07:04 +0100 Subject: [PATCH 27/36] Added action_date Signed-off-by: snipe --- app/Models/Loggable.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Models/Loggable.php b/app/Models/Loggable.php index 15157cc6a..93b82f840 100644 --- a/app/Models/Loggable.php +++ b/app/Models/Loggable.php @@ -309,6 +309,7 @@ trait Loggable $log->item_id = $this->id; } $log->location_id = null; + $log->action_date = date('Y-m-d H:i:s'); $log->note = $note; $log->created_by = $created_by; $log->logaction('create'); @@ -336,6 +337,7 @@ trait Loggable $log->note = $note; $log->target_id = null; $log->created_at = date('Y-m-d H:i:s'); + $log->action_date = date('Y-m-d H:i:s'); $log->filename = $filename; $log->logaction('uploaded'); From b51939ae7609e5ca2285699b66eba0a48fc1730a Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 04:07:31 +0100 Subject: [PATCH 28/36] Derp. Use correct model info Signed-off-by: snipe --- app/Http/Requests/Traits/MayContainCustomFields.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/Http/Requests/Traits/MayContainCustomFields.php b/app/Http/Requests/Traits/MayContainCustomFields.php index c0b624c33..2b12ccff6 100644 --- a/app/Http/Requests/Traits/MayContainCustomFields.php +++ b/app/Http/Requests/Traits/MayContainCustomFields.php @@ -11,12 +11,10 @@ trait MayContainCustomFields public function withValidator($validator) { + // In case the model is being changed via form + if (request()->has('model_id')!='') { - // For auditing and some other non-standard things where $this is only the form submission and may not have the asset info - // In case the model is being changed - if (request()->input('model_id')!='') { - - $asset_model = AssetModel::find(request()->route('asset')->model_id); + $asset_model = AssetModel::find(request()->input('model_id')); // or if we have it available to route-model-binding } elseif ((request()->route('asset') && (request()->route('asset')->model_id))) { From 849da2fb633b259894c26ab446e5b35b43a1eefb Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 04:15:59 +0100 Subject: [PATCH 29/36] Use correct audit icon Signed-off-by: snipe --- app/Presenters/ActionlogPresenter.php | 4 ++++ resources/views/layouts/default.blade.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/Presenters/ActionlogPresenter.php b/app/Presenters/ActionlogPresenter.php index 834b88e75..4b7aefc87 100644 --- a/app/Presenters/ActionlogPresenter.php +++ b/app/Presenters/ActionlogPresenter.php @@ -102,6 +102,10 @@ class ActionlogPresenter extends Presenter return 'fas fa-sticky-note'; } + if ($this->action_type == 'audit') { + return 'fas fa-clipboard-check'; + } + return 'fa-solid fa-rotate-right'; } diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index 82f7eca50..10340a6c1 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -526,7 +526,7 @@ dir="{{ Helper::determineLanguageDirection() }}"> @can('audit', \App\Models\Asset::class) - + {{ trans('general.audit_due') }} {{ (isset($total_due_and_overdue_for_audit)) ? $total_due_and_overdue_for_audit : '' }} From c46a9a773d8e92d9356f8f4ceb54289b7cdfdb55 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:08:08 +0100 Subject: [PATCH 30/36] Fixed admin string Signed-off-by: snipe --- app/Http/Controllers/ReportsController.php | 2 +- app/Presenters/AccessoryPresenter.php | 2 +- app/Presenters/LocationPresenter.php | 2 +- resources/views/accessories/view.blade.php | 2 +- resources/views/consumables/view.blade.php | 4 ++-- resources/views/licenses/view.blade.php | 2 +- resources/views/locations/view.blade.php | 2 +- resources/views/reports/asset_maintenances.blade.php | 2 +- resources/views/users/view.blade.php | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index bd22562a6..33afac531 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -243,7 +243,7 @@ class ReportsController extends Controller $header = [ trans('general.date'), - trans('general.admin'), + trans('general.created_by'), trans('general.action'), trans('general.type'), trans('general.item'), diff --git a/app/Presenters/AccessoryPresenter.php b/app/Presenters/AccessoryPresenter.php index 6d423bed7..86b4d1bc0 100644 --- a/app/Presenters/AccessoryPresenter.php +++ b/app/Presenters/AccessoryPresenter.php @@ -229,7 +229,7 @@ class AccessoryPresenter extends Presenter 'field' => 'created_by', 'searchable' => false, 'sortable' => false, - 'title' => trans('general.admin'), + 'title' => trans('general.created_by'), 'visible' => false, 'formatter' => 'usersLinkObjFormatter', ], diff --git a/app/Presenters/LocationPresenter.php b/app/Presenters/LocationPresenter.php index 42f56a309..e36a38682 100644 --- a/app/Presenters/LocationPresenter.php +++ b/app/Presenters/LocationPresenter.php @@ -262,7 +262,7 @@ class LocationPresenter extends Presenter 'field' => 'created_by', 'searchable' => false, 'sortable' => false, - 'title' => trans('general.admin'), + 'title' => trans('general.created_by'), 'visible' => false, 'formatter' => 'usersLinkObjFormatter', ], diff --git a/resources/views/accessories/view.blade.php b/resources/views/accessories/view.blade.php index a4f4656bf..41ffbb95f 100644 --- a/resources/views/accessories/view.blade.php +++ b/resources/views/accessories/view.blade.php @@ -125,7 +125,7 @@ {{ trans('general.record_created') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/consumables/view.blade.php b/resources/views/consumables/view.blade.php index 93031b63c..4ef82efc8 100644 --- a/resources/views/consumables/view.blade.php +++ b/resources/views/consumables/view.blade.php @@ -421,7 +421,7 @@ {{ trans('general.date') }} {{ trans('general.notes') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} @@ -472,7 +472,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/licenses/view.blade.php b/resources/views/licenses/view.blade.php index ffcc94621..6d7b406df 100755 --- a/resources/views/licenses/view.blade.php +++ b/resources/views/licenses/view.blade.php @@ -498,7 +498,7 @@ {{ trans('general.record_created') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} diff --git a/resources/views/locations/view.blade.php b/resources/views/locations/view.blade.php index 6ee75d6bf..345a40a3d 100644 --- a/resources/views/locations/view.blade.php +++ b/resources/views/locations/view.blade.php @@ -434,7 +434,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.item') }} {{ trans('general.target') }} diff --git a/resources/views/reports/asset_maintenances.blade.php b/resources/views/reports/asset_maintenances.blade.php index 3da2e798f..406375ccd 100644 --- a/resources/views/reports/asset_maintenances.blade.php +++ b/resources/views/reports/asset_maintenances.blade.php @@ -46,7 +46,7 @@ {{ trans('general.location') }} {{ trans('admin/hardware/form.default_location') }} {{ trans('admin/asset_maintenances/table.is_warranty') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('admin/asset_maintenances/form.notes') }} diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index 7ee3c4926..e77ab3bd2 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -1003,7 +1003,7 @@ {{ trans('general.signature') }} @endif {{ trans('admin/hardware/table.serial') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('admin/settings/general.login_ip') }} {{ trans('admin/settings/general.login_user_agent') }} {{ trans('general.action_source') }} From 31c9ffa32bdae752bd18080fa32ea2622357d8f6 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:08:42 +0100 Subject: [PATCH 31/36] Added audits method Signed-off-by: snipe --- app/Models/Asset.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/Models/Asset.php b/app/Models/Asset.php index a0e113279..7fd80eaf0 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -690,6 +690,21 @@ class Asset extends Depreciable ->withTrashed(); } + + /** + * Get the list of audits for this asset + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function audits() + { + return $this->assetlog()->where('action_type', '=', 'audit') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** * Get the list of checkins for this asset * From 744e844291a46e16d2170e6483afd604c287f291 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:08:58 +0100 Subject: [PATCH 32/36] Added audit tab Signed-off-by: snipe --- resources/views/hardware/view.blade.php | 66 ++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index e7bd3460e..ba6556c41 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -111,6 +111,22 @@ @endif + @if ($asset->audits->count() > 0) +
  • + + + + + +
  • + @endif +
  • -
    + +
    + +
    +
    + + + + + + + + + + + + + + + +
    {{ trans('general.date') }}{{ trans('general.created_by') }}{{ trans('general.file_name') }}{{ trans('general.notes') }}{{ trans('general.download') }}{{ trans('admin/hardware/table.changed')}}{{ trans('admin/settings/general.login_ip') }}{{ trans('admin/settings/general.login_user_agent') }}{{ trans('general.action_source') }}
    +
    +
    +
    + + +
    @@ -1419,7 +1481,7 @@ {{ trans('admin/hardware/table.icon') }} {{ trans('general.date') }} - {{ trans('general.admin') }} + {{ trans('general.created_by') }} {{ trans('general.action') }} {{ trans('general.file_name') }} {{ trans('general.item') }} From 05e66c33ee942b82b3025ce10bf932a7cc46182c Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:09:08 +0100 Subject: [PATCH 33/36] Added audits string Signed-off-by: snipe --- resources/lang/en-US/general.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index e20c7dfd5..72d577ff6 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -31,6 +31,7 @@ return [ 'accept_assets_menu' => 'Accept Assets', 'accept_item' => 'Accept Item', 'audit' => 'Audit', + 'audits' => 'Audits', 'audit_report' => 'Audit Log', 'assets' => 'Assets', 'assets_audited' => 'assets audited', From 83562cfa8315db4ee49cf5a7710b691fdc6522da Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:23:39 +0100 Subject: [PATCH 34/36] Added additional searchable relations to activity report Signed-off-by: snipe --- app/Models/Actionlog.php | 68 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index 3a8fcf1cb..778100482 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -69,7 +69,25 @@ class Actionlog extends SnipeModel 'company' => ['name'], 'adminuser' => ['first_name','last_name','username', 'email'], 'user' => ['first_name','last_name','username', 'email'], - 'assets' => ['asset_tag','name', 'serial'], + 'assets' => ['asset_tag','name', 'serial', 'order_number'], + 'assets.model' => ['name', 'model_number', 'eol'], + 'assets.model.category' => ['name'], + 'assets.model.manufacturer' => ['name'], + 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order'], + 'licenses.category' => ['name'], + 'licenses.supplier' => ['name'], + 'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no'], + 'consumables.category' => ['name'], + 'consumables.location' => ['name'], + 'consumables.supplier' => ['name'], + 'components' => ['name'], + 'components.category' => ['name'], + 'components.location' => ['name'], + 'components.supplier' => ['name'], + 'accessories' => ['name'], + 'accessories.category' => ['name'], + 'accessories.location' => ['name'], + 'accessories.supplier' => ['name'], ]; /** @@ -134,6 +152,54 @@ class Actionlog extends SnipeModel return $this->hasMany(\App\Models\Asset::class, 'id', 'item_id'); } + /** + * Establishes the actionlog -> license relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function licenses() + { + return $this->hasMany(\App\Models\License::class, 'id', 'item_id'); + } + + /** + * Establishes the actionlog -> consumable relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function consumables() + { + return $this->hasMany(\App\Models\Consumable::class, 'id', 'item_id'); + } + + /** + * Establishes the actionlog -> consumable relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function accessories() + { + return $this->hasMany(\App\Models\Accessory::class, 'id', 'item_id'); + } + + /** + * Establishes the actionlog -> components relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function components() + { + return $this->hasMany(\App\Models\Component::class, 'id', 'item_id'); + } + /** * Establishes the actionlog -> item type relationship * From 733ef9e23b200d8d903c7394887c22c49d874120 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:44:49 +0100 Subject: [PATCH 35/36] Few more Signed-off-by: snipe --- app/Models/Actionlog.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index 778100482..fbeec61c5 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -69,25 +69,25 @@ class Actionlog extends SnipeModel 'company' => ['name'], 'adminuser' => ['first_name','last_name','username', 'email'], 'user' => ['first_name','last_name','username', 'email'], - 'assets' => ['asset_tag','name', 'serial', 'order_number'], - 'assets.model' => ['name', 'model_number', 'eol'], - 'assets.model.category' => ['name'], - 'assets.model.manufacturer' => ['name'], + 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes'], + 'assets.model' => ['name', 'model_number', 'eol', 'notes'], + 'assets.model.category' => ['name', 'notes'], + 'assets.model.manufacturer' => ['name', 'notes'], 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order'], - 'licenses.category' => ['name'], + 'licenses.category' => ['name', 'notes'], 'licenses.supplier' => ['name'], 'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no'], - 'consumables.category' => ['name'], - 'consumables.location' => ['name'], - 'consumables.supplier' => ['name'], - 'components' => ['name'], - 'components.category' => ['name'], - 'components.location' => ['name'], - 'components.supplier' => ['name'], + 'consumables.category' => ['name', 'notes'], + 'consumables.location' => ['name', 'notes'], + 'consumables.supplier' => ['name', 'notes'], + 'components' => ['name', 'notes'], + 'components.category' => ['name', 'notes'], + 'components.location' => ['name', 'notes'], + 'components.supplier' => ['name', 'notes'], 'accessories' => ['name'], 'accessories.category' => ['name'], - 'accessories.location' => ['name'], - 'accessories.supplier' => ['name'], + 'accessories.location' => ['name', 'notes'], + 'accessories.supplier' => ['name', 'notes'], ]; /** From b2e0f48ed9c6db4e908b45c5aadd6d64decaebc2 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 8 Apr 2025 05:50:03 +0100 Subject: [PATCH 36/36] Added purchase date Signed-off-by: snipe --- app/Models/Actionlog.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/Models/Actionlog.php b/app/Models/Actionlog.php index fbeec61c5..dd86ae25c 100755 --- a/app/Models/Actionlog.php +++ b/app/Models/Actionlog.php @@ -57,7 +57,9 @@ class Actionlog extends SnipeModel 'user_agent', 'item_type', 'target_type', - 'action_source' + 'action_source', + 'created_at', + 'action_date', ]; /** @@ -69,22 +71,22 @@ class Actionlog extends SnipeModel 'company' => ['name'], 'adminuser' => ['first_name','last_name','username', 'email'], 'user' => ['first_name','last_name','username', 'email'], - 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes'], + 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'], 'assets.model' => ['name', 'model_number', 'eol', 'notes'], 'assets.model.category' => ['name', 'notes'], 'assets.model.manufacturer' => ['name', 'notes'], - 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order'], + 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'], 'licenses.category' => ['name', 'notes'], 'licenses.supplier' => ['name'], - 'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no'], + 'consumables' => ['name', 'notes', 'order_number', 'model_number', 'item_no', 'purchase_date'], 'consumables.category' => ['name', 'notes'], 'consumables.location' => ['name', 'notes'], 'consumables.supplier' => ['name', 'notes'], - 'components' => ['name', 'notes'], + 'components' => ['name', 'notes', 'purchase_date'], 'components.category' => ['name', 'notes'], 'components.location' => ['name', 'notes'], 'components.supplier' => ['name', 'notes'], - 'accessories' => ['name'], + 'accessories' => ['name', 'purchase_date'], 'accessories.category' => ['name'], 'accessories.location' => ['name', 'notes'], 'accessories.supplier' => ['name', 'notes'],