Fixes #1190 - added basic audit workflow

This commit is contained in:
snipe 2017-08-25 10:04:19 -07:00
parent af6f208c43
commit 16f57e16cb
18 changed files with 413 additions and 5 deletions

View file

@ -31,6 +31,7 @@ use TCPDF;
use Validator; use Validator;
use View; use View;
/** /**
* This class controls all actions related to assets for * This class controls all actions related to assets for
* the Snipe-IT Asset Management application. * the Snipe-IT Asset Management application.
@ -496,4 +497,33 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error'))); return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error')));
} }
/**
* Mark an asset as audited
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
* @since [v4.0]
* @return JsonResponse
*/
public function audit(Request $request, $id) {
$this->authorize('audit', Asset::class);
$rules = array(
'id' => 'required'
);
$validator = \Validator::make($request->all(), $rules);
$asset = Asset::findOrFail($id);
$asset->next_audit_date = $request->input('next_audit_date');
if ($asset->save()) {
$asset->logAudit(request('note'));
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.audit.success')));
}
}
} }

View file

@ -36,6 +36,10 @@ class ReportsController extends Controller
->where('item_type','=',"App\\Models\\".ucwords($request->input('item_type'))); ->where('item_type','=',"App\\Models\\".ucwords($request->input('item_type')));
} }
if ($request->has('action_type')) {
$actionlogs = $actionlogs->where('action_type','=',$request->input('action_type'))->orderBy('created_at', 'desc');
}
$allowed_columns = [ $allowed_columns = [
'id', 'id',
'created_at' 'created_at'

View file

@ -595,9 +595,12 @@ class AssetsController extends Controller
*/ */
public function show($assetId = null) public function show($assetId = null)
{ {
$asset = Asset::withTrashed()->find($assetId); $asset = Asset::withTrashed()->find($assetId);
$settings = Setting::getSettings();
$this->authorize('view', $asset); $this->authorize('view', $asset);
$settings = Setting::getSettings();
$audit_log = Actionlog::where('action_type','=','audit')->where('item_id','=',$assetId)->where('item_type','=',Asset::class)->orderBy('created_at','DESC')->first();
if (isset($asset)) { if (isset($asset)) {
@ -617,7 +620,8 @@ class AssetsController extends Controller
'url' => route('qr_code/hardware', $asset->id) 'url' => route('qr_code/hardware', $asset->id)
); );
return view('hardware/view', compact('asset', 'qr_code', 'settings'))->with('use_currency', $use_currency); return view('hardware/view', compact('asset', 'qr_code', 'settings'))
->with('use_currency', $use_currency)->with('audit_log',$audit_log);
} }
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist', compact('id'))); return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist', compact('id')));
@ -1233,4 +1237,29 @@ class AssetsController extends Controller
// Redirect to the asset management page with error // Redirect to the asset management page with error
return redirect()->to("hardware/bulk-checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors); return redirect()->to("hardware/bulk-checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
} }
public function audit(Request $request, $id)
{
$this->authorize('audit', Asset::class);
$dt = Carbon::now()->addMonths(12)->toDateString();
$asset = Asset::findOrFail($id);
return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt);
}
public function auditStore(Request $request, $id)
{
$this->authorize('audit', Asset::class);
$asset = Asset::findOrFail($id);
$asset->next_audit_date = $request->input('next_audit_date');
if ($asset->save()) {
$asset->logAudit(request('note'));
return redirect()->to("hardware")->with('success', trans('admin/hardware/message.audit.success'));
}
}
} }

View file

@ -271,6 +271,20 @@ class ReportsController extends Controller
} }
/**
* Displays audit report.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return View
*/
public function audit()
{
return view('reports/audit');
}
/** /**
* Displays activity report. * Displays activity report.
* *

View file

@ -29,6 +29,7 @@ class ActionlogsTransformer
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($actionlog->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($actionlog->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($actionlog->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($actionlog->updated_at, 'datetime'),
'next_audit_date' => ($actionlog->itemType()=='asset') ? Helper::getFormattedDateObject($actionlog->item->next_audit_date, 'datetime'): null,
'action_type' => $actionlog->present()->actionType(), 'action_type' => $actionlog->present()->actionType(),
'admin' => ($actionlog->user) ? [ 'admin' => ($actionlog->user) ? [
'id' => (int) $actionlog->user->id, 'id' => (int) $actionlog->user->id,

View file

@ -7,6 +7,7 @@ use App\Models\Asset;
use App\Models\CheckoutRequest; use App\Models\CheckoutRequest;
use App\Models\User; use App\Models\User;
use App\Notifications\CheckinNotification; use App\Notifications\CheckinNotification;
use App\Notifications\AuditNotification;
use App\Notifications\CheckoutNotification; use App\Notifications\CheckoutNotification;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -120,6 +121,38 @@ trait Loggable
return $log; return $log;
} }
/**
* @author A. Gianotto <snipe@snipe.net>
* @since [v4.0]
* @return \App\Models\Actionlog
*/
public function logAudit($note)
{
$log = new Actionlog;
if (static::class == LicenseSeat::class) {
$log->item_type = License::class;
$log->item_id = $this->license_id;
} else {
$log->item_type = static::class;
$log->item_id = $this->id;
}
$log->location_id = null;
$log->note = $note;
$log->user_id = Auth::user()->id;
$log->logaction('audit');
$params = [
'item' => $log->item,
'admin' => $log->user,
'note' => $note
];
Setting::getSettings()->notify(new AuditNotification($params));
return $log;
}
/** /**
* @author Daniel Meltzer <parallelgrapefruit@gmail.com * @author Daniel Meltzer <parallelgrapefruit@gmail.com
* @since [v3.5] * @since [v3.5]

View file

@ -0,0 +1,90 @@
<?php
namespace App\Notifications;
use App\Models\Setting;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class AuditNotification extends Notification
{
use Queueable;
/**
* @var
*/
private $params;
/**
* Create a new notification instance.
*
* @param $params
*/
public function __construct($params)
{
//
$this->params = $params;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
$notifyBy = [];
if (Setting::getSettings()->slack_endpoint) {
$notifyBy[] = 'slack';
}
return $notifyBy;
}
public function toSlack($notifiable)
{
return (new SlackMessage)
->success()
->content(class_basename(get_class($this->params['item'])) . " Audited")
->attachment(function ($attachment) use ($notifiable) {
$item = $this->params['item'];
$admin_user = $this->params['admin'];
$fields = [
'By' => '<'.$admin_user->present()->viewUrl().'|'.$admin_user->present()->fullName().'>'
];
array_key_exists('note', $this->params) && $fields['Notes'] = $this->params['note'];
$attachment->title($item->name, $item->present()->viewUrl())
->fields($fields);
});
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', 'https://laravel.com')
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View file

@ -20,6 +20,7 @@ class AssetObserver
if ((isset($asset->getOriginal()['assigned_to'])) && ($asset->getAttributes()['assigned_to'] == $asset->getOriginal()['assigned_to']) if ((isset($asset->getOriginal()['assigned_to'])) && ($asset->getAttributes()['assigned_to'] == $asset->getOriginal()['assigned_to'])
&& ($asset->getAttributes()['next_audit_date'] == $asset->getOriginal()['next_audit_date'])
&& ($asset->getAttributes()['last_checkout'] == $asset->getOriginal()['last_checkout']) && ($asset->getAttributes()['last_checkout'] == $asset->getOriginal()['last_checkout'])
&& ($asset->getAttributes()['status_id'] == $asset->getOriginal()['status_id'])) && ($asset->getAttributes()['status_id'] == $asset->getOriginal()['status_id']))
{ {

View file

@ -85,8 +85,8 @@ return array(
array( array(
'permission' => 'assets.audit', 'permission' => 'assets.audit',
'label' => 'Audit ', 'label' => 'Audit ',
'note' => '', 'note' => 'Allows the user to mark an asset as physically inventoried.',
'display' => false, 'display' => true,
), ),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddAuditingTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('assets', function (Blueprint $table) {
$table->date('next_audit_date')->nullable()->default(NULL);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('assets', function (Blueprint $table) {
$table->dropColumn('next_audit_date');
});
}
}

View file

@ -24,6 +24,12 @@ return array(
'success' => 'Asset restored successfully.' 'success' => 'Asset restored successfully.'
), ),
'audit' => array(
'error' => 'Asset audit was unsuccessful. Please try again.',
'success' => 'Asset audit successfully logged.'
),
'deletefile' => array( 'deletefile' => array(
'error' => 'File not deleted. Please try again.', 'error' => 'File not deleted. Please try again.',
'success' => 'File successfully deleted.', 'success' => 'File successfully deleted.',

View file

@ -18,6 +18,8 @@
'asset_report' => 'Asset Report', 'asset_report' => 'Asset Report',
'asset_tag' => 'Asset Tag', 'asset_tag' => 'Asset Tag',
'assets_available' => 'assets available', 'assets_available' => 'assets available',
'audit' => 'Audit',
'audit_report' => 'Audit Log',
'assets' => 'Assets', 'assets' => 'Assets',
'avatar_delete' => 'Delete Avatar', 'avatar_delete' => 'Delete Avatar',
'avatar_upload' => 'Upload Avatar', 'avatar_upload' => 'Upload Avatar',
@ -117,6 +119,8 @@
'moreinfo' => 'More Info', 'moreinfo' => 'More Info',
'name' => 'Name', 'name' => 'Name',
'next' => 'Next', 'next' => 'Next',
'next_audit_date' => 'Next Audit Date',
'last_audit' => 'Last Audit',
'new' => 'new!', 'new' => 'new!',
'no_depreciation' => 'No Depreciation', 'no_depreciation' => 'No Depreciation',
'no_results' => 'No Results.', 'no_results' => 'No Results.',

View file

@ -0,0 +1,82 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('general.audit') }}
@parent
@stop
{{-- Page content --}}
@section('content')
<style>
.input-group {
padding-left: 0px !important;
}
</style>
<div class="row">
<!-- left column -->
<div class="col-md-7">
<div class="box box-default">
<form class="form-horizontal" method="post" action="" autocomplete="off">
<div class="box-header with-border">
<h3 class="box-title"> {{ trans('admin/hardware/form.tag') }} {{ $asset->asset_tag }}</h3>
</div>
<div class="box-body">
{{csrf_field()}}
@if ($asset->model->name)
<!-- Asset name -->
<div class="form-group {{ $errors->has('name') ? '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>
</div>
</div>
@endif
<!-- Asset Name -->
<div class="form-group {{ $errors->has('name') ? '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>
</div>
</div>
<!-- Next Audit -->
<div class="form-group {{ $errors->has('next_audit_date') ? 'error' : '' }}">
{{ Form::label('name', trans('admin/hardware/form.checkout_date'), array('class' => 'col-md-3 control-label')) }}
<div class="col-md-9">
<div class="input-group date col-md-5" data-provide="datepicker" data-date-format="yyyy-mm-dd">
<input type="text" class="form-control" placeholder="{{ trans('general.next_audit_date') }}" name="next_audit_date" id="next_audit_date" value="{{ Input::old('next_audit_date', $next_audit_date) }}">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
</div>
{!! $errors->first('next_audit_date', '<span class="alert-msg"><i class="fa fa-times"></i> :message</span>') !!}
</div>
</div>
<!-- Note -->
<div class="form-group {{ $errors->has('note') ? '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">{{ Input::old('note', $asset->note) }}</textarea>
{!! $errors->first('note', '<span class="alert-msg"><i class="fa fa-times"></i> :message</span>') !!}
</div>
</div>
</div> <!--/.box-body-->
<div class="box-footer">
<a class="btn btn-link" href="{{ URL::previous() }}"> {{ trans('button.cancel') }}</a>
<button type="submit" class="btn btn-success pull-right"><i class="fa fa-check icon-white"></i> {{ trans('general.checkout') }}</button>
</div>
</form>
</div>
</div> <!--/.col-md-7-->
</div>
@stop

View file

@ -23,6 +23,7 @@
@endif @endif
<li role="presentation"><a href="{{ route('hardware.edit', $asset->id) }}">{{ trans('admin/hardware/general.edit') }}</a></li> <li role="presentation"><a href="{{ route('hardware.edit', $asset->id) }}">{{ trans('admin/hardware/general.edit') }}</a></li>
<li role="presentation"><a href="{{ route('clone/hardware', $asset->id) }}">{{ trans('admin/hardware/general.clone') }}</a></li> <li role="presentation"><a href="{{ route('clone/hardware', $asset->id) }}">{{ trans('admin/hardware/general.clone') }}</a></li>
<li role="presentation"><a href="{{ route('asset.audit.create', $asset->id) }}">{{ trans('general.audit') }}</a></li>
</ul> </ul>
</div> </div>
@endcan @endcan
@ -112,6 +113,18 @@
<td>{{ $asset->serial }}</td> <td>{{ $asset->serial }}</td>
</tr> </tr>
@endif @endif
@if ($audit_log->created_at)
<tr>
<td>{{ trans('general.last_audit') }}</td>
<td> {{ \App\Helpers\Helper::getFormattedDateObject($audit_log->created_at, 'date', false) }} (by {{ link_to_route('users.show', $audit_log->user->present()->fullname(), [$audit_log->user->id]) }})</td>
</tr>
@endif
@if ($asset->next_audit_date)
<tr>
<td>{{ trans('general.next_audit_date') }}</td>
<td> {{ \App\Helpers\Helper::getFormattedDateObject($asset->next_audit_date, 'date', false) }}</td>
</tr>
@endif
@if ($asset->model->manufacturer) @if ($asset->model->manufacturer)
<tr> <tr>

View file

@ -0,0 +1,46 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('general.audit_report') }}
@parent
@stop
{{-- Page content --}}
@section('content')
<div class="row">
<div class="col-md-12">
<div class="box box-default">
<div class="box-body">
<table
name="activityReport"
data-toolbar="#toolbar"
class="table table-striped snipe-table"
id="table"
data-url="{{ route('api.activity.index', ['action_type' => 'audit']) }}"
data-cookie="true"
data-cookie-id-table="activityReportTable">
<thead>
<tr>
<th data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter"></th>
<th class="col-sm-3" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th class="col-sm-2" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th class="col-sm-2" data-field="action_type">{{ trans('general.action') }}</th>
<th class="col-sm-3" data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th class="col-sm-1" data-field="note">{{ trans('general.notes') }}</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
@stop
@section('moar_scripts')
@include ('partials.bootstrap-table', ['exportFile' => 'activity-export', 'search' => true])
@stop

View file

@ -214,6 +214,12 @@ Route::group(['prefix' => 'v1','namespace' => 'Api'], function () {
Route::group(['prefix' => 'hardware'], function () { Route::group(['prefix' => 'hardware'], function () {
Route::post('audit/{id}', [
'as' => 'api.asset.audit',
'uses' => 'AssetsController@audit'
]);
Route::post('{asset_id}/checkout', Route::post('{asset_id}/checkout',
[ [
'as' => 'api.assets.checkout', 'as' => 'api.assets.checkout',

View file

@ -274,6 +274,11 @@ Route::group([ 'prefix' => 'account', 'middleware' => ['auth']], function () {
Route::group(['middleware' => ['auth']], function () { Route::group(['middleware' => ['auth']], function () {
Route::get('reports/audit', [
'as' => 'reports.audit',
'uses' => 'ReportsController@audit'
]);
Route::get( Route::get(
'reports/depreciation', 'reports/depreciation',
[ 'as' => 'reports/depreciation', 'uses' => 'ReportsController@getDeprecationReport' ] [ 'as' => 'reports/depreciation', 'uses' => 'ReportsController@getDeprecationReport' ]
@ -316,7 +321,7 @@ Route::group(['middleware' => ['auth']], function () {
Route::get( Route::get(
'reports/activity', 'reports/activity',
[ 'as' => 'reports/activity', 'uses' => 'ReportsController@getActivityReport' ] [ 'as' => 'reports.activity', 'uses' => 'ReportsController@getActivityReport' ]
); );

View file

@ -17,6 +17,16 @@ Route::group(
'parameters' => ['maintenance' => 'maintenance_id', 'asset' => 'asset_id'] 'parameters' => ['maintenance' => 'maintenance_id', 'asset' => 'asset_id']
]); ]);
Route::get('audit/{id}', [
'as' => 'asset.audit.create',
'uses' => 'AssetsController@audit'
]);
Route::post('audit/{id}', [
'as' => 'asset.audit.store',
'uses' => 'AssetsController@auditStore'
]);
Route::get('history', [ Route::get('history', [
'as' => 'asset.import-history', 'as' => 'asset.import-history',
@ -111,6 +121,8 @@ Route::group(
'as' => 'hardware/bulkcheckout', 'as' => 'hardware/bulkcheckout',
'uses' => 'AssetsController@postBulkCheckout' 'uses' => 'AssetsController@postBulkCheckout'
]); ]);
}); });