diff --git a/app/Http/Controllers/Assets/BulkAssetsController.php b/app/Http/Controllers/Assets/BulkAssetsController.php index 45ca5bab7c..d0c66d4b11 100644 --- a/app/Http/Controllers/Assets/BulkAssetsController.php +++ b/app/Http/Controllers/Assets/BulkAssetsController.php @@ -7,6 +7,8 @@ use App\Helpers\Helper; use App\Http\Controllers\CheckInOutRequest; use App\Http\Controllers\Controller; use App\Models\Asset; +use App\Models\AssetModel; +use App\Models\Statuslabel; use App\Models\Setting; use App\View\Label; use Illuminate\Http\Request; @@ -23,6 +25,13 @@ class BulkAssetsController extends Controller /** * Display the bulk edit page. * + * This method is super weird because it's kinda of like a controller within a controller. + * It's main function is to determine what the bulk action in, and then return a view with + * the information that view needs, be it bulk delete, bulk edit, restore, etc. + * + * This is something that made sense at the time, but sort of doesn't make sense now. A JS front-end to determine form + * action would make a lot more sense here and make things a lot more clear. + * * @author [A. Gianotto] [] * @return View * @internal param int $assetId @@ -32,7 +41,10 @@ class BulkAssetsController extends Controller public function edit(Request $request) { $this->authorize('view', Asset::class); - + + /** + * No asset IDs were passed + */ if (! $request->filled('ids')) { return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected')); } @@ -41,59 +53,52 @@ class BulkAssetsController extends Controller $bulk_back_url = request()->headers->get('referer'); session(['bulk_back_url' => $bulk_back_url]); - $asset_ids = array_values(array_unique($request->input('ids'))); - - //custom fields logic - $asset_custom_field = Asset::with(['model.fieldset.fields', 'model'])->whereIn('id', $asset_ids)->whereHas('model', function ($query) { - return $query->where('fieldset_id', '!=', null); - })->get(); - $models = $asset_custom_field->unique('model_id'); + $asset_ids = $request->input('ids'); + $assets = Asset::with('assignedTo', 'location', 'model')->find($asset_ids); + + $models = $assets->unique('model_id'); $modelNames = []; foreach($models as $model) { $modelNames[] = $model->model->name; - } + } if ($request->filled('bulk_actions')) { + + switch ($request->input('bulk_actions')) { case 'labels': $this->authorize('view', Asset::class); - $assets_found = Asset::find($asset_ids); - - if ($assets_found->isEmpty()){ - return redirect()->back(); - } return (new Label) - ->with('assets', $assets_found) + ->with('assets', $assets) ->with('settings', Setting::getSettings()) ->with('bulkedit', true) ->with('count', 0); case 'delete': $this->authorize('delete', Asset::class); - $assets = Asset::with('assignedTo', 'location')->find($asset_ids); - $assets->each(function ($asset) { - $this->authorize('delete', $asset); + $assets->each(function ($assets) { + $this->authorize('delete', $assets); }); return view('hardware/bulk-delete')->with('assets', $assets); case 'restore': $this->authorize('update', Asset::class); - $assets = Asset::withTrashed()->find($asset_ids); + $assets = Asset::withTrashed()->find($asset_ids); $assets->each(function ($asset) { $this->authorize('delete', $asset); }); - return view('hardware/bulk-restore')->with('assets', $assets); case 'edit': $this->authorize('update', Asset::class); + return view('hardware/bulk') ->with('assets', $asset_ids) ->with('statuslabel_list', Helper::statusLabelList()) - ->with('models', $models->pluck(['model'])) + ->with('models', $models->pluck(['model'])) ->with('modelNames', $modelNames); } } @@ -117,25 +122,31 @@ class BulkAssetsController extends Controller // Get the back url from the session and then destroy the session $bulk_back_url = route('hardware.index'); + if ($request->session()->has('bulk_back_url')) { $bulk_back_url = $request->session()->pull('bulk_back_url'); } $custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); + - if (Session::exists('ids')) { - $assets = Session::get('ids'); - } elseif (! $request->filled('ids') || count($request->input('ids')) <= 0) { + if (! $request->filled('ids') || count($request->input('ids')) == 0) { return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected')); } - - $assets = array_keys($request->input('ids')); - - if ($request->anyFilled($custom_field_columns)) { - $custom_fields_present = true; - } else { - $custom_fields_present = false; - } + + + $assets = Asset::whereIn('id', array_keys($request->input('ids')))->get(); + + + + /** + * If ANY of these are filled, prepare to update the values on the assets. + * + * Additional checks will be needed for some of them to make sure the values + * make sense (for example, changing the status ID to something incompatible with + * its checkout status. + */ + if (($request->filled('purchase_date')) || ($request->filled('expected_checkin')) || ($request->filled('purchase_cost')) @@ -154,22 +165,32 @@ class BulkAssetsController extends Controller || ($request->anyFilled($custom_field_columns)) ) { - foreach ($assets as $assetId) { + // Let's loop through those assets and build an update array + foreach ($assets as $asset) { $this->update_array = []; + /** + * Leave out model_id and status here because we do math on that later. We have to do some extra + * validation and checks on those two. + * + * It's tempting to make these match the request check above, but some of these values require + * extra work to make sure the data makes sense. + */ $this->conditionallyAddItem('purchase_date') ->conditionallyAddItem('expected_checkin') ->conditionallyAddItem('order_number') ->conditionallyAddItem('requestable') - ->conditionallyAddItem('status_id') ->conditionallyAddItem('supplier_id') ->conditionallyAddItem('warranty_months') ->conditionallyAddItem('next_audit_date'); foreach ($custom_field_columns as $key => $custom_field_column) { $this->conditionallyAddItem($custom_field_column); - } + } + /** + * Blank out fields that were requested to be blanked out via checkbox + */ if ($request->input('null_purchase_date')=='1') { $this->update_array['purchase_date'] = null; } @@ -186,7 +207,6 @@ class BulkAssetsController extends Controller $this->update_array['purchase_cost'] = $request->input('purchase_cost'); } - if ($request->filled('company_id')) { $this->update_array['company_id'] = $request->input('company_id'); if ($request->input('company_id') == 'clear') { @@ -194,88 +214,129 @@ class BulkAssetsController extends Controller } } + /** + * We're trying to change the model ID - we need to do some extra checks here to make sure + * the custom field values work for the custom fieldset rules around this asset. Uniqueness + * and requiredness across the fieldset is particularly important, since those are + * fieldset-specific attributes. + */ + if ($request->filled('model_id')) { + $this->update_array['model_id'] = AssetModel::find($request->input('model_id'))->id; + } + + /** + * We're trying to change the status ID - we need to do some extra checks here to + * make sure the status label type is one that makes sense for the state of the asset, + * for example, we shouldn't be able to make an asset archived if it's currently assigned + * to someone/something. + */ + if ($request->filled('status_id')) { + $updated_status = Statuslabel::find($request->input('status_id')); + + // We cannot assign a non-deployable status type if the asset is already assigned. + // This could probably be added to a form request. + // If the asset isn't assigned, we don't care what the status is. + // Otherwise we need to make sure the status type is still a deployable one. + if ( + ($asset->assigned_to == '') + || ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1') + ) { + $this->update_array['status_id'] = $updated_status->id; + } + + } + + /** + * We're changing the location ID - figure out which location we should apply + * this change to: + * + * 0 - RTD location only + * 1 - location ID and RTD location ID + * 2 - location ID only + * + * Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe + */ if ($request->filled('rtd_location_id')) { + if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) { $this->update_array['rtd_location_id'] = $request->input('rtd_location_id'); } + if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) { $this->update_array['location_id'] = $request->input('rtd_location_id'); $this->update_array['rtd_location_id'] = $request->input('rtd_location_id'); } + if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) { $this->update_array['location_id'] = $request->input('rtd_location_id'); } + } + + /** + * ------------------------------------------------------------------------------ + * ANYTHING that happens past this foreach + * WILL NOT BE logged in the edit log_meta data + * ------------------------------------------------------------------------------ + */ $changed = []; - $asset = Asset::find($assetId); foreach ($this->update_array as $key => $value) { + if ($this->update_array[$key] != $asset->{$key}) { $changed[$key]['old'] = $asset->{$key}; $changed[$key]['new'] = $this->update_array[$key]; } + } - - if ($custom_fields_present) { - $model = $asset->model()->first(); + /** + * Start all the custom fields shenanigans + */ - // Use the rules of the new model fieldsets if the model changed - if ($request->filled('model_id')) { - $this->update_array['model_id'] = $request->input('model_id'); - $model = \App\Models\AssetModel::find($request->input('model_id')); - } + // Does the model have a fieldset? + if ($asset->model->fieldset) { + foreach ($asset->model->fieldset->fields as $field) { + if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) { + $decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column}); - // Make sure this model is valid - $assetCustomFields = ($model) ? $model->fieldset : null; - - if ($assetCustomFields && $assetCustomFields->fields) { - - foreach ($assetCustomFields->fields as $field) { - - if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted=='1')) { - $decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column}); - - /* - * Check if the decrypted existing value is different from one we just submitted - * and if not, pull it out of the object since it shouldn't really be updating at all. - * If we don't do this, it will try to re-encrypt it, and the same value encrypted two - * different times will have different values, so it will *look* like it was updated - * but it wasn't. - */ - if ($decrypted_old != $this->update_array[$field->db_column]) { - $asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]); - } else { - /* - * Remove the encrypted custom field from the update_array, since nothing changed - */ - unset($this->update_array[$field->db_column]); - unset($asset->{$field->db_column}); - } - + /* + * Check if the decrypted existing value is different from one we just submitted + * and if not, pull it out of the object since it shouldn't really be updating at all. + * If we don't do this, it will try to re-encrypt it, and the same value encrypted two + * different times will have different values, so it will *look* like it was updated + * but it wasn't. + */ + if ($decrypted_old != $this->update_array[$field->db_column]) { + $asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]); + } else { /* - * These custom fields aren't encrypted, just carry on as usual + * Remove the encrypted custom field from the update_array, since nothing changed */ + unset($this->update_array[$field->db_column]); + unset($asset->{$field->db_column}); + } + + /* + * These custom fields aren't encrypted, just carry on as usual + */ + } else { + + if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) { + + // Check if this is an array, and if so, flatten it + if (is_array($this->update_array[$field->db_column])) { + $asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]); } else { - - - if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) { - - // Check if this is an array, and if so, flatten it - if (is_array($this->update_array[$field->db_column])) { - $asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]); - } else { - $asset->{$field->db_column} = $this->update_array[$field->db_column]; - } - } + $asset->{$field->db_column} = $this->update_array[$field->db_column]; } + } + } - } // endforeach - } // end custom field check - } // end custom fields handler - + } // endforeach + } // Check if it passes validation, and then try to save diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 4e5a25974e..f23ff52f64 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -266,7 +266,7 @@ class Asset extends Depreciable /** * Determines if an asset is available for checkout. - * This checks to see if the it's checked out to an invalid (deleted) user + * This checks to see if it's checked out to an invalid (deleted) user * OR if the assigned_to and deleted_at fields on the asset are empty AND * that the status is deployable * @@ -753,7 +753,7 @@ class Asset extends Depreciable } /** - * Establishes the asset -> status relationship + * Establishes the asset -> license seats relationship * * @author [A. Gianotto] [] * @since [v4.0] diff --git a/database/migrations/2023_07_13_052204_denormalized_eol_and_add_column_for_explicit_date_to_assets.php b/database/migrations/2023_07_13_052204_denormalized_eol_and_add_column_for_explicit_date_to_assets.php index 982bd8ac0d..4781af7f0b 100644 --- a/database/migrations/2023_07_13_052204_denormalized_eol_and_add_column_for_explicit_date_to_assets.php +++ b/database/migrations/2023_07_13_052204_denormalized_eol_and_add_column_for_explicit_date_to_assets.php @@ -3,6 +3,7 @@ use App\Models\Asset; use Carbon\CarbonImmutable; use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -50,7 +51,7 @@ class DenormalizedEolAndAddColumnForExplicitDateToAssets extends Migration ->whereNotNull('model_id') ->join('models', 'assets.model_id', '=', 'models.id') ->update([ - 'asset_eol_date' => DB::raw('DATE_ADD(purchase_date, INTERVAL ' . DB::getTablePrefix() . 'models.eol MONTH)') + 'asset_eol_date' => $this->eolUpdateExpression(), ]); } @@ -66,4 +67,22 @@ class DenormalizedEolAndAddColumnForExplicitDateToAssets extends Migration $table->dropColumn('eol_explicit'); }); } + + /** + * This method returns the correct database expression for either + * mysql, postgres, or sqlite depending on the driver being used. + */ + private function eolUpdateExpression(): Expression + { + if (DB::getDriverName() === 'sqlite') { + return DB::raw("DATE(purchase_date, '+' || (SELECT eol FROM " . DB::getTablePrefix() . "models WHERE models.id = assets.model_id) || ' months')"); + } + + if (DB::getDriverName() === 'pgsql') { + return DB::raw("date(purchase_date + interval '1 month' * (SELECT eol FROM " . DB::getTablePrefix() . "models WHERE models.id = assets.model_id))"); + } + + // Default to MySQL's method + return DB::raw('DATE_ADD(purchase_date, INTERVAL ' . DB::getTablePrefix() . 'models.eol MONTH)'); + } } diff --git a/resources/lang/en/admin/settings/general.php b/resources/lang/en/admin/settings/general.php index 64d0aef53e..c8d6306036 100644 --- a/resources/lang/en/admin/settings/general.php +++ b/resources/lang/en/admin/settings/general.php @@ -9,6 +9,7 @@ return [ 'ad_append_domain_help' => 'User isn\'t required to write "username@domain.local", they can just type "username".', 'admin_cc_email' => 'CC Email', 'admin_cc_email_help' => 'If you would like to send a copy of checkin/checkout emails that are sent to users to an additional email account, enter it here. Otherwise leave this field blank.', + 'admin_settings' => 'Admin Settings', 'is_ad' => 'This is an Active Directory server', 'alerts' => 'Alerts', 'alert_title' => 'Update Notification Settings', diff --git a/resources/lang/en/admin/users/table.php b/resources/lang/en/admin/users/table.php index 21e2154280..b8b919bf28 100644 --- a/resources/lang/en/admin/users/table.php +++ b/resources/lang/en/admin/users/table.php @@ -21,6 +21,7 @@ return array( 'manager' => 'Manager', 'managed_locations' => 'Managed Locations', 'name' => 'Name', + 'nogroup' => 'No groups have been created yet. To add one, visit: ', 'notes' => 'Notes', 'password_confirm' => 'Confirm Password', 'password' => 'Password', diff --git a/resources/lang/en/general.php b/resources/lang/en/general.php index a568e00436..1f2f4322cc 100644 --- a/resources/lang/en/general.php +++ b/resources/lang/en/general.php @@ -491,5 +491,6 @@ return [ 'upload_error' => 'Error uploading file. Please check that there are no empty rows and that no column names are duplicated.', 'copy_to_clipboard' => 'Copy to Clipboard', 'copied' => 'Copied!', + 'status_compatibility' => 'If assets are already assigned, they cannot be changed to a non-deployable status type and this value change will be skipped.', ]; diff --git a/resources/views/hardware/bulk-restore.blade.php b/resources/views/hardware/bulk-restore.blade.php index caabe9fab8..e4bfb6e4dd 100644 --- a/resources/views/hardware/bulk-restore.blade.php +++ b/resources/views/hardware/bulk-restore.blade.php @@ -30,7 +30,7 @@ {{ trans('admin/hardware/table.id') }} - {{ trans('admin/hardware/table.name') }} + {{ trans('admin/hardware/form.name') }} {{ trans('admin/hardware/table.location')}} diff --git a/resources/views/hardware/bulk.blade.php b/resources/views/hardware/bulk.blade.php index a7e52dfa56..fa4680e2e8 100755 --- a/resources/views/hardware/bulk.blade.php +++ b/resources/views/hardware/bulk.blade.php @@ -19,18 +19,22 @@

{{ trans('admin/hardware/form.bulk_update_help') }}

-
- {{ trans_choice('admin/hardware/form.bulk_update_warn', count($assets), ['asset_count' => count($assets)]) }} - @if (count($models) > 0) - {{ trans_choice('admin/hardware/form.bulk_update_with_custom_field', count($models), ['asset_model_count' => count($models)]) }} - @endif -
+
{{ csrf_field() }}
+ +
+ {{ trans_choice('admin/hardware/form.bulk_update_warn', count($assets), ['asset_count' => count($assets)]) }} + + @if (count($models) > 0) + {{ trans_choice('admin/hardware/form.bulk_update_with_custom_field', count($models), ['asset_model_count' => count($models)]) }} + @endif +
+
@@ -76,6 +80,7 @@
{{ Form::select('status_id', $statuslabel_list , old('status_id'), array('class'=>'select2', 'style'=>'width:100%', 'aria-label'=>'status_id')) }} +

{{ trans('general.status_compatibility') }}

{!! $errors->first('status_id', '') !!}
@@ -189,8 +194,8 @@
- @include("models/custom_fields_form_bulk_edit",["models" => $models]) - + @include("models/custom_fields_form_bulk_edit",["models" => $models]) + @foreach ($assets as $key => $value) @endforeach diff --git a/resources/views/hardware/view.blade.php b/resources/views/hardware/view.blade.php index 5a043ffc40..badb4b1e6b 100755 --- a/resources/views/hardware/view.blade.php +++ b/resources/views/hardware/view.blade.php @@ -1206,15 +1206,15 @@ {{ trans('admin/hardware/table.icon') }} - {{ trans('general.date') }} - {{ trans('general.admin') }} - {{ trans('general.action') }} - {{ trans('general.item') }} - {{ trans('general.target') }} - {{ trans('general.notes') }} - {{ trans('general.signature') }} - {{ trans('general.download') }} - {{ trans('admin/hardware/table.changed')}} + {{ trans('general.date') }} + {{ trans('general.admin') }} + {{ trans('general.action') }} + {{ trans('general.item') }} + {{ trans('general.target') }} + {{ trans('general.notes') }} + {{ trans('general.signature') }} + {{ trans('general.download') }} + {{ trans('admin/hardware/table.changed')}} diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index a8b5ddf05e..60a882ea62 100755 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -553,7 +553,7 @@ @endif @else -

No groups have been created yet. Visit Admin Settings > Permission Groups to add one.

+

{{ trans('admin/users/table.nogroup') }} {{ trans('admin/settings/general.admin_settings') }} > {{ trans('general.groups') }}

@endif diff --git a/resources/views/users/view.blade.php b/resources/views/users/view.blade.php index ea5e8d80df..0ed5c93b80 100755 --- a/resources/views/users/view.blade.php +++ b/resources/views/users/view.blade.php @@ -638,13 +638,14 @@ @endif + @if($user->getUserTotalCost()->total_user_cost > 0)
-
{{ trans('admin/users/table.total_assets_cost') }}
- - + + @endif - - -