Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe 2024-05-16 16:37:47 +01:00
commit d691f3b315
12 changed files with 201 additions and 53 deletions

View file

@ -1032,25 +1032,39 @@ class AssetsController extends Controller
{
$this->authorize('audit', Asset::class);
$rules = [
'asset_tag' => 'required',
'location_id' => 'exists:locations,id|nullable|numeric',
'next_audit_date' => 'date|nullable',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all()));
}
$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);
}
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
if ($asset) {
// We don't want to log this as a normal update, so let's bypass that
/**
* 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;
@ -1066,8 +1080,12 @@ class AssetsController extends Controller
$asset->last_audit_date = date('Y-m-d H:i:s');
if ($asset->save()) {
$log = $asset->logAudit(request('note'), request('location_id'));
/**
* 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),
@ -1075,9 +1093,23 @@ class AssetsController extends Controller
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
], 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);
}
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag'=> e($request->input('asset_tag'))], 'Asset with tag '.e($request->input('asset_tag')).' not found'));
// 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);
}

View file

@ -6,7 +6,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Manufacturer;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log;
use App\Models\Asset;
use App\Models\AssetModel;
@ -308,7 +308,8 @@ class AssetsController extends Controller
$asset->status_id = $request->input('status_id', null);
$asset->warranty_months = $request->input('warranty_months', null);
$asset->purchase_cost = $request->input('purchase_cost', null);
$asset->purchase_date = $request->input('purchase_date', null);
$asset->purchase_date = $request->input('purchase_date', null);
$asset->next_audit_date = $request->input('next_audit_date', null);
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) {
$asset->purchase_date = $request->input('purchase_date', null);
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
@ -862,7 +863,7 @@ class AssetsController extends Controller
}
public function auditStore(Request $request, $id)
public function auditStore(UploadFileRequest $request, $id)
{
$this->authorize('audit', Asset::class);
@ -879,7 +880,21 @@ class AssetsController extends Controller
$asset = Asset::findOrFail($id);
// We don't want to log this as a normal update, so let's bypass that
/**
* 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 = $request->input('next_audit_date');
@ -888,29 +903,26 @@ class AssetsController extends Controller
// 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') {
Log::debug('update location in audit');
$asset->location_id = $request->input('location_id');
}
if ($asset->save()) {
$file_name = '';
// Upload an image, if attached
if ($request->hasFile('image')) {
$path = 'private_uploads/audits';
if (! Storage::exists($path)) {
Storage::makeDirectory($path, 775);
}
$upload = $image = $request->file('image');
$ext = $image->getClientOriginalExtension();
$file_name = 'audit-'.str_random(18).'.'.$ext;
Storage::putFileAs($path, $upload, $file_name);
}
/**
* 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()) {
// Create the image (if one was chosen.)
if ($request->hasFile('image')) {
$file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image'));
}
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name);
return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
}
return redirect()->back()->withInput()->withErrors($asset->getErrors());
}
public function getRequestedIndex($user_id = null)

View file

@ -99,7 +99,8 @@ class Asset extends Depreciable
'last_checkin' => 'nullable|date_format:Y-m-d H:i:s',
'expected_checkin' => 'nullable|date',
'last_audit_date' => 'nullable|date_format:Y-m-d H:i:s',
'next_audit_date' => 'nullable|date|after:last_audit_date',
// 'next_audit_date' => 'nullable|date|after:last_audit_date',
'next_audit_date' => 'nullable|date',
'location_id' => 'nullable|exists:locations,id',
'rtd_location_id' => 'nullable|exists:locations,id',
'purchase_date' => 'nullable|date|date_format:Y-m-d',
@ -907,6 +908,23 @@ class Asset extends Depreciable
}
/**
* Determine whether this asset's next audit date is before the last audit date
*
* @return bool
* @since [v6.4.1]
* @author [A. Gianotto] [<snipe@snipe.net>]
* */
public function checkInvalidNextAuditDate()
{
if (($this->last_audit_date) && ($this->next_audit_date) && ($this->last_audit_date > $this->next_audit_date)) {
return true;
}
return false;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
@ -944,6 +962,25 @@ class Asset extends Depreciable
* -----------------------------------------------
**/
/**
* Make sure the next_audit_date is formatted as Y-m-d.
*
* This is kind of dumb and confusing, since we already cast it that way AND it's a date field
* in the database, but here we are.
*
* @param $value
* @return void
*/
public function getNextAuditDateAttribute($value)
{
return $this->attributes['next_audit_date'] = $value ? Carbon::parse($value)->format('Y-m-d') : null;
}
public function setNextAuditDateAttribute($value)
{
$this->attributes['next_audit_date'] = $value ? Carbon::parse($value)->format('Y-m-d') : null;
}
/**
* This sets the requestable to a boolean 0 or 1. This accounts for forms or API calls that
* explicitly pass the requestable field but it has a null or empty value.

View file

@ -5,8 +5,11 @@ return [
'undeployable' => '<strong>Warning: </strong> This asset has been marked as currently undeployable.
If this status has changed, please update the asset status.',
'does_not_exist' => 'Asset does not exist.',
'does_not_exist_var'=> 'Asset with tag :asset_tag not found.',
'no_tag' => 'No asset tag provided.',
'does_not_exist_or_not_requestable' => 'That asset does not exist or is not requestable.',
'assoc_users' => 'This asset is currently checked out to a user and cannot be deleted. Please check the asset in first, and then try deleting again. ',
'warning_audit_date_mismatch' => 'This asset\'s next audit date (:next_audit_date) is before the last audit date (:last_audit_date). Please update the next audit date.',
'create' => [
'error' => 'Asset was not created, please try again. :(',
@ -31,7 +34,7 @@ return [
],
'audit' => [
'error' => 'Asset audit was unsuccessful. Please try again.',
'error' => 'Asset audit unsuccessful: :error ',
'success' => 'Asset audit successfully logged.',
],

View file

@ -202,6 +202,8 @@ return [
'new_password' => 'New Password',
'next' => 'Next',
'next_audit_date' => 'Next Audit Date',
'next_audit_date_help' => 'If you use auditing in your organization, this is usually automatically calculated based on the asset&apos;s last audit date and audit frequency (in <code>Admin Settings &gt; Alerts</code>) and you can leave this blank. You can manually set this date here if you need to, but it must be later than the last audit date. ',
'audit_images_help' => 'You can find audit images in the asset\'s history tab.',
'no_email' => 'No email address associated with this user',
'last_audit' => 'Last Audit',
'new' => 'new!',

View file

@ -34,7 +34,7 @@
{{csrf_field()}}
@if ($asset->model->name)
<!-- Asset name -->
<div class="form-group {{ $errors->has('name') ? 'error' : '' }}">
<div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
{{ Form::label('name', trans('admin/hardware/form.model'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-8">
<p class="form-control-static">{{ $asset->model->name }}</p>
@ -43,7 +43,7 @@
@endif
<!-- Asset Name -->
<div class="form-group {{ $errors->has('name') ? 'error' : '' }}">
<div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
{{ Form::label('name', trans('admin/hardware/form.name'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-8">
<p class="form-control-static">{{ $asset->name }}</p>
@ -66,21 +66,40 @@
</div>
<!-- Show last audit date -->
<div class="form-group">
<label class="control-label col-md-3">
{{ trans('general.last_audit') }}
</label>
<div class="col-md-8">
<p class="form-control-static">
@if ($asset->last_audit_date)
{{ Helper::getFormattedDateObject($asset->last_audit_date, 'datetime', false) }}
@else
{{ trans('admin/settings/general.none') }}
@endif
</p>
</div>
</div>
<!-- Next Audit -->
<div class="form-group {{ $errors->has('next_audit_date') ? 'error' : '' }}">
<div class="form-group{{ $errors->has('next_audit_date') ? ' has-error' : '' }}">
{{ Form::label('name', trans('general.next_audit_date'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-9">
<div class="col-md-8">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-clear-btn="true">
<input type="text" class="form-control" placeholder="{{ trans('general.next_audit_date') }}" name="next_audit_date" id="next_audit_date" value="{{ old('next_audit_date', $next_audit_date) }}">
<span class="input-group-addon"><i class="fas fa-calendar" aria-hidden="true"></i></span>
</div>
{!! $errors->first('next_audit_date', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<p class="help-block">{!! trans('general.next_audit_date_help') !!}</p>
</div>
</div>
<!-- Note -->
<div class="form-group {{ $errors->has('note') ? 'error' : '' }}">
<div class="form-group{{ $errors->has('note') ? ' has-error' : '' }}">
{{ Form::label('note', trans('admin/hardware/form.notes'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-8">
<textarea class="col-md-6 form-control" id="note" name="note">{{ old('note', $asset->note) }}</textarea>
@ -88,13 +107,8 @@
</div>
</div>
<!-- Images -->
@include ('partials.forms.edit.image-upload')
<!-- Audit Image -->
@include ('partials.forms.edit.image-upload', ['help_text' => trans('general.audit_images_help')])
</div> <!--/.box-body-->

View file

@ -164,6 +164,8 @@
{!! $errors->first('next_audit_date', '<span class="alert-msg" aria-hidden="true">
<i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
<div class="col-md-5">
<label class="form-control">
@ -171,6 +173,9 @@
{{ trans_choice('general.set_to_null', count($assets), ['asset_count' => count($assets)]) }}
</label>
</div>
<div class="col-md-8 col-md-offset-3">
<p class="help-block">{!! trans('general.next_audit_date_help') !!}</p>
</div>
</div>
<!-- Requestable -->

View file

@ -109,6 +109,29 @@
@include ('partials.forms.edit.name', ['translated_name' => trans('admin/hardware/form.name')])
@include ('partials.forms.edit.warranty')
<!-- Datepicker -->
<div class="form-group{{ $errors->has('next_audit_date') ? ' has-error' : '' }}">
<label class="col-md-3 control-label">
{{ trans('general.next_audit_date') }}
</label>
<div class="input-group col-md-4">
<div class="input-group date" data-provide="datepicker" data-date-clear-btn="true" data-date-format="yyyy-mm-dd" data-autoclose="true">
<input type="text" class="form-control" placeholder="{{ trans('general.select_date') }}" name="next_audit_date" id="next_audit_date" value="{{ old('next_audit_date', $item->next_audit_date) }}" readonly style="background-color:inherit" maxlength="10">
<span class="input-group-addon"><i class="fas fa-calendar" aria-hidden="true"></i></span>
</div>
</div>
<div class="col-md-8 col-md-offset-3">
{!! $errors->first('next_audit_date', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<p class="help-block">{!! trans('general.next_audit_date_help') !!}</p>
</div>
</div>
<!-- byod checkbox -->
<div class="form-group">
<div class="col-md-7 col-md-offset-3">

View file

@ -23,9 +23,7 @@
<!-- left column -->
<div class="col-md-6">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ trans('general.bulkaudit') }} </h2>
</div>
<div class="box-body">
{{csrf_field()}}
@ -185,7 +183,7 @@
} else {
var messages = '';
}
$('#audited tbody').prepend("<tr class='danger'><td>" + asset_tag + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
$('#audited tbody').prepend("<tr class='danger'><td>" + data.payload.asset_tag + "</td><td>" + messages + "</td><td><i class='fas fa-times text-danger'></i></td></tr>");
}
function incrementOnSuccess() {

View file

@ -19,6 +19,23 @@
</div>
@endif
@if ($asset->checkInvalidNextAuditDate())
<div class="col-md-12">
<div class="callout callout-warning">
<p><strong>{{ trans('general.warning',
[
'warning' => trans('admin/hardware/message.warning_audit_date_mismatch',
[
'last_audit_date' => Helper::getFormattedDateObject($asset->last_audit_date, 'date', false),
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date, 'date', false)
]
)
]
) }}</strong></p>
</div>
</div>
@endif
@if ($asset->deleted_at!='')
<div class="col-md-12">
<div class="alert alert-danger">
@ -237,7 +254,8 @@
</strong>
</div>
<div class="col-md-6">
{{ \App\Helpers\Helper::getFormattedDateObject($audit_log->created_at, 'date', false) }}
{!! $asset->checkInvalidNextAuditDate() ? '<i class="fas fa-exclamation-triangle text-orange" aria-hidden="true"></i>' : '' !!}
{{ Helper::getFormattedDateObject($audit_log->created_at, 'date', false) }}
@if ($audit_log->user)
(by {{ link_to_route('users.show', $audit_log->user->present()->fullname(), [$audit_log->user->id]) }})
@endif
@ -254,6 +272,7 @@
</strong>
</div>
<div class="col-md-6">
{!! $asset->checkInvalidNextAuditDate() ? '<i class="fas fa-exclamation-triangle text-orange" aria-hidden="true"></i>' : '' !!}
{{ Helper::getFormattedDateObject($asset->next_audit_date, 'date', false) }}
</div>
</div>

View file

@ -32,7 +32,10 @@
</label>
<span class='label label-default' id="uploadFile-info"></span>
<p class="help-block" id="uploadFile-status">{{ trans('general.image_filetypes_help', ['size' => Helper::file_upload_max_size_readable()]) }}</p>
<p class="help-block" id="uploadFile-status">{{ trans('general.image_filetypes_help', ['size' => Helper::file_upload_max_size_readable()]) }} {{ $help_text ?? '' }}</p>
{!! $errors->first('image', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
<div class="col-md-4 col-md-offset-3" aria-hidden="true">

View file

@ -103,7 +103,7 @@ class AssetsBulkEditTest extends TestCase
$this->assertEquals($company2->id, $asset->company_id);
$this->assertEquals(7890, $asset->order_number);
$this->assertEquals(36, $asset->warranty_months);
$this->assertEquals('2025-01-01', $asset->next_audit_date->format('Y-m-d'));
$this->assertEquals('2025-01-01', $asset->next_audit_date);
// shouldn't requestable be cast as a boolean??? it's not.
$this->assertEquals(1, $asset->requestable);
});