Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe 2023-11-29 09:41:05 +00:00
commit df2b52c537
11 changed files with 202 additions and 116 deletions

View file

@ -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] [<snipe@snipe.net>]
* @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

View file

@ -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] [<snipe@snipe.net>]
* @since [v4.0]

View file

@ -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)');
}
}

View file

@ -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',

View file

@ -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',

View file

@ -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.',
];

View file

@ -30,7 +30,7 @@
<tr>
<td></td>
<td>{{ trans('admin/hardware/table.id') }}</td>
<td>{{ trans('admin/hardware/table.name') }}</td>
<td>{{ trans('admin/hardware/form.name') }}</td>
<td>{{ trans('admin/hardware/table.location')}}</td>
</tr>
</thead>

View file

@ -19,18 +19,22 @@
<p>{{ trans('admin/hardware/form.bulk_update_help') }}</p>
<div class="callout callout-warning">
<i class="fas fa-exclamation-triangle"></i> {{ 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
</div>
<form class="form-horizontal" method="post" action="{{ route('hardware/bulksave') }}" autocomplete="off" role="form">
{{ csrf_field() }}
<div class="box box-default">
<div class="box-body">
<div class="callout callout-warning">
<i class="fas fa-exclamation-triangle"></i> {{ 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
</div>
<!-- Purchase Date -->
<div class="form-group {{ $errors->has('purchase_date') ? ' has-error' : '' }}">
<label for="purchase_date" class="col-md-3 control-label">{{ trans('admin/hardware/form.date') }}</label>
@ -76,6 +80,7 @@
</label>
<div class="col-md-7">
{{ Form::select('status_id', $statuslabel_list , old('status_id'), array('class'=>'select2', 'style'=>'width:100%', 'aria-label'=>'status_id')) }}
<p class="help-block">{{ trans('general.status_compatibility') }}</p>
{!! $errors->first('status_id', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
@ -189,8 +194,8 @@
</div>
</div>
@include("models/custom_fields_form_bulk_edit",["models" => $models])
@include("models/custom_fields_form_bulk_edit",["models" => $models])
@foreach ($assets as $key => $value)
<input type="hidden" name="ids[{{ $value }}]" value="1">
@endforeach

View file

@ -1206,15 +1206,15 @@
<thead>
<tr>
<th data-visible="true" data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">{{ trans('admin/hardware/table.icon') }}</th>
<th class="col-sm-2" data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th class="col-sm-1" data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th class="col-sm-1" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-2" data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th class="col-sm-2" data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
<th class="col-sm-2" data-field="note">{{ trans('general.notes') }}</th>
<th class="col-md-3" data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
<th class="col-md-3" data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
<th class="col-sm-2" data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
<th data-visible="true" data-field="action_date" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
<th data-visible="true" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th data-visible="true" data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
<th data-field="note">{{ trans('general.notes') }}</th>
<th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
<th data-visible="false" data-field="file" data-visible="false" data-formatter="fileUploadFormatter">{{ trans('general.download') }}</th>
<th data-field="log_meta" data-visible="true" data-formatter="changeLogFormatter">{{ trans('admin/hardware/table.changed')}}</th>
</tr>
</thead>
</table>

View file

@ -553,7 +553,7 @@
</div>
@endif
@else
<p>No groups have been created yet. Visit <code>Admin Settings > Permission Groups</code> to add one.</p>
<p>{{ trans('admin/users/table.nogroup') }} <code>{{ trans('admin/settings/general.admin_settings') }} <i class="fa fa-cogs"></i> > {{ trans('general.groups') }} <i class="fas fa-user-friends"></i></code> </p>
@endif
</div>

View file

@ -638,13 +638,14 @@
</div>
@endif
@if($user->getUserTotalCost()->total_user_cost > 0)
<div class="row">
<div class="col-md-3">
{{ trans('admin/users/table.total_assets_cost') }}
</div>
<div class="col-md-9">
{{Helper::formatCurrencyOutput($user->getUserTotalCost()->total_user_cost)}}
<a id="optional_info" class="text-primary">
<i class="fa fa-caret-right fa-2x" id="optional_info_icon"></i>
<strong>{{ trans('admin/hardware/form.optional_infos') }}</strong>
@ -658,13 +659,10 @@
{{trans('general.accessories').': '.Helper::formatCurrencyOutput($user->getUserTotalCost()->accessory_cost)}}<br>
</div>
</div>
</div>
</div><!--/.row-->
@endif
</div> <!--/end striped container-->
</div> <!-- end col-md-9 -->
</div> <!--/.row-->
</div><!-- /.tab-pane -->