mirror of
https://github.com/snipe/snipe-it.git
synced 2025-01-22 19:21:58 -08:00
* Starting work on asset history importer. * Starting work on asset history importer. * Added checkin target. * Last change... importing history should also probably be an admin only task. * Added caching for user and asset queries. * Updated cache keepalive time to DateTimeInterface * Updated cache keepalive time to DateTimeInterface
This commit is contained in:
parent
5624ea14e7
commit
3f7d2aebc7
|
@ -22,6 +22,8 @@ use Image;
|
|||
use Input;
|
||||
use Lang;
|
||||
use League\Csv\Reader;
|
||||
use League\Csv\Statement;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Log;
|
||||
use Mail;
|
||||
use Paginator;
|
||||
|
@ -516,7 +518,7 @@ class AssetsController extends Controller
|
|||
*/
|
||||
public function getImportHistory()
|
||||
{
|
||||
$this->authorize('checkout', Asset::class);
|
||||
$this->authorize('admin');
|
||||
return view('hardware/history');
|
||||
}
|
||||
|
||||
|
@ -526,8 +528,8 @@ class AssetsController extends Controller
|
|||
* This needs a LOT of love. It's done very inelegantly right now, and there are
|
||||
* a ton of optimizations that could (and should) be done.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.3]
|
||||
* @author [herroworrd]
|
||||
* @since [v5.0]
|
||||
* @return View
|
||||
*/
|
||||
public function postImportHistory(Request $request)
|
||||
|
@ -536,128 +538,103 @@ class AssetsController extends Controller
|
|||
ini_set("auto_detect_line_endings", '1');
|
||||
}
|
||||
|
||||
$requiredcolumns = ['Asset Tag', 'Checkout Date', 'Checkin Date', 'Full Name'];
|
||||
|
||||
$csv = Reader::createFromPath(Input::file('user_import_csv'));
|
||||
$csv->setNewline("\r\n");
|
||||
//get the first row, usually the CSV header
|
||||
|
||||
$csv->setHeaderOffset(0);
|
||||
$results = $csv->getRecords();
|
||||
$item = array();
|
||||
|
||||
//Stop here if we don't have the columns we need
|
||||
if(count(array_intersect($requiredcolumns, $csv->getHeader())) != count($requiredcolumns)) {
|
||||
$status['error'][]['csv'][]['msg'] = 'Headers do not match';
|
||||
return view('hardware/history')->with('status', $status);
|
||||
}
|
||||
$statement = (new Statement())
|
||||
->orderBy(\Closure::fromCallable([$this, 'sortByName']));
|
||||
|
||||
$results = $statement->process($csv);
|
||||
|
||||
$status = array();
|
||||
$status['error'] = array();
|
||||
$status['success'] = array();
|
||||
$base_username = null;
|
||||
$cachetime = Carbon::now()->addSeconds(120);
|
||||
|
||||
foreach ($results as $record) {
|
||||
|
||||
foreach ($results as $row) {
|
||||
if (is_array($row)) {
|
||||
$row = array_change_key_case($row, CASE_LOWER);
|
||||
$asset_tag = Helper::array_smart_fetch($row, "asset tag");
|
||||
if (!array_key_exists($asset_tag, $item)) {
|
||||
$item[$asset_tag] = array();
|
||||
}
|
||||
$batch_counter = count($item[$asset_tag]);
|
||||
$asset_tag = $record['Asset Tag'];
|
||||
|
||||
$item[$asset_tag][$batch_counter]['checkout_date'] = Carbon::parse(Helper::array_smart_fetch($row, "date"))->format('Y-m-d H:i:s');
|
||||
|
||||
$item[$asset_tag][$batch_counter]['asset_tag'] = Helper::array_smart_fetch($row, "asset tag");
|
||||
$item[$asset_tag][$batch_counter]['name'] = Helper::array_smart_fetch($row, "name");
|
||||
$item[$asset_tag][$batch_counter]['email'] = Helper::array_smart_fetch($row, "email");
|
||||
|
||||
if ($asset = Asset::where('asset_tag', '=', $asset_tag)->first()) {
|
||||
$item[$asset_tag][$batch_counter]['asset_id'] = $asset->id;
|
||||
|
||||
$base_username = User::generateFormattedNameFromFullName($item[$asset_tag][$batch_counter]['name'], Setting::getSettings()->username_format);
|
||||
$user = User::where('username', '=', $base_username['username']);
|
||||
$user_query = ' on username '.$base_username['username'];
|
||||
|
||||
if ($request->input('match_firstnamelastname')=='1') {
|
||||
$firstnamedotlastname = User::generateFormattedNameFromFullName($item[$asset_tag][$batch_counter]['name'], 'firstname.lastname');
|
||||
$item[$asset_tag][$batch_counter]['username'][] = $firstnamedotlastname['username'];
|
||||
$user->orWhere('username', '=', $firstnamedotlastname['username']);
|
||||
$user_query .= ', or on username '.$firstnamedotlastname['username'];
|
||||
}
|
||||
|
||||
if ($request->input('match_flastname')=='1') {
|
||||
$flastname = User::generateFormattedNameFromFullName( $item[$asset_tag][$batch_counter]['name'], 'filastname');
|
||||
$item[$asset_tag][$batch_counter]['username'][] = $flastname['username'];
|
||||
$user->orWhere('username', '=', $flastname['username']);
|
||||
$user_query .= ', or on username '.$flastname['username'];
|
||||
}
|
||||
if ($request->input('match_firstname')=='1') {
|
||||
$firstname = User::generateFormattedNameFromFullName( $item[$asset_tag][$batch_counter]['name'], 'firstname');
|
||||
$item[$asset_tag][$batch_counter]['username'][] = $firstname['username'];
|
||||
$user->orWhere('username', '=', $firstname['username']);
|
||||
$user_query .= ', or on username '.$firstname['username'];
|
||||
}
|
||||
if ($request->input('match_email')=='1') {
|
||||
if ($item[$asset_tag][$batch_counter]['email']=='') {
|
||||
$item[$asset_tag][$batch_counter]['username'][] = $user_email = User::generateEmailFromFullName($item[$asset_tag][$batch_counter]['name']);
|
||||
$user->orWhere('username', '=', $user_email);
|
||||
$user_query .= ', or on username '.$user_email;
|
||||
}
|
||||
}
|
||||
|
||||
// A matching user was found
|
||||
if ($user = $user->first()) {
|
||||
$item[$asset_tag][$batch_counter]['checkedout_to'] = $user->id;
|
||||
$item[$asset_tag][$batch_counter]['user_id'] = $user->id;
|
||||
|
||||
Actionlog::firstOrCreate(array(
|
||||
'item_id' => $asset->id,
|
||||
'item_type' => Asset::class,
|
||||
'user_id' => Auth::user()->id,
|
||||
'note' => 'Checkout imported by '.Auth::user()->present()->fullName().' from history importer',
|
||||
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
|
||||
'target_type' => User::class,
|
||||
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
|
||||
'action_type' => 'checkout',
|
||||
));
|
||||
|
||||
$asset->assigned_to = $user->id;
|
||||
|
||||
if ($asset->save()) {
|
||||
$status['success'][]['asset'][$asset_tag]['msg'] = 'Asset successfully matched for '.Helper::array_smart_fetch($row, "name").$user_query.' on '.$item[$asset_tag][$batch_counter]['checkout_date'];
|
||||
} else {
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Asset and user was matched but could not be saved.';
|
||||
}
|
||||
} else {
|
||||
$item[$asset_tag][$batch_counter]['checkedout_to'] = null;
|
||||
$status['error'][]['user'][Helper::array_smart_fetch($row, "name")]['msg'] = 'User does not exist so no checkin log was created.';
|
||||
}
|
||||
} else {
|
||||
$item[$asset_tag][$batch_counter]['asset_id'] = null;
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Asset does not exist so no match was attempted.';
|
||||
}
|
||||
try {
|
||||
$checkoutdate = Carbon::parse($record['Checkout Date'])->format('Y-m-d H:i:s');
|
||||
$checkindate = Carbon::parse($record['Checkin Date'])->format('Y-m-d H:i:s');
|
||||
}
|
||||
catch (\Exception $err) {
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Your dates are screwed up. Format needs to be Y-m-d H:i:s';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through and backfill the checkins
|
||||
foreach ($item as $key => $asset_batch) {
|
||||
$total_in_batch = count($asset_batch);
|
||||
for ($x = 0; $x < $total_in_batch; $x++) {
|
||||
$next = $x + 1;
|
||||
if($asset = Cache::remember('asset:' . $asset_tag, $cachetime, function () use( &$asset_tag) {
|
||||
$tocache = Asset::where('asset_tag', '=', $asset_tag)->value('id');
|
||||
return is_null($tocache) ? false : $tocache;}))
|
||||
{
|
||||
//we've found our asset, now lets look for a user
|
||||
if($base_username != User::generateFormattedNameFromFullName($record['Full Name'], Setting::getSettings()->username_format)) {
|
||||
|
||||
// Only do this if a matching user was found
|
||||
if ((array_key_exists('checkedout_to', $asset_batch[$x])) && ($asset_batch[$x]['checkedout_to']!='')) {
|
||||
if (($total_in_batch > 1) && ($x < $total_in_batch) && (array_key_exists($next, $asset_batch))) {
|
||||
$checkin_date = Carbon::parse($asset_batch[$next]['checkout_date'])->subDay(1)->format('Y-m-d H:i:s');
|
||||
$asset_batch[$x]['real_checkin'] = $checkin_date;
|
||||
$base_username = User::generateFormattedNameFromFullName($record['Full Name'], Setting::getSettings()->username_format);
|
||||
|
||||
if(!$user = Cache::remember('user:' . $base_username['username'], $cachetime, function () use( &$base_username) {
|
||||
$tocache = User::where('username', '=', $base_username['username'])->value('id');
|
||||
return is_null($tocache) ? false : $tocache;}))
|
||||
{
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Asset was found but user (' . $record['Full Name'] . ') not matched';
|
||||
$base_username = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if($checkoutdate < $checkindate) {
|
||||
|
||||
Actionlog::firstOrCreate(array(
|
||||
'item_id' => $asset_batch[$x]['asset_id'],
|
||||
'item_id' => $asset,
|
||||
'item_type' => Asset::class,
|
||||
'user_id' => Auth::user()->id,
|
||||
'note' => 'Checkin imported by ' . Auth::user()->present()->fullName() . ' from history importer',
|
||||
'target_id' => null,
|
||||
'created_at' => $checkin_date,
|
||||
'note' => 'Historical record added by ' . Auth::user()->present()->fullName(),
|
||||
'target_id' => $user,
|
||||
'target_type' => User::class,
|
||||
'created_at' => $checkoutdate,
|
||||
'action_type' => 'checkout',
|
||||
));
|
||||
|
||||
Actionlog::firstOrCreate(array(
|
||||
'item_id' => $asset,
|
||||
'item_type' => Asset::class,
|
||||
'user_id' => Auth::user()->id,
|
||||
'note' => 'Historical record added by ' . Auth::user()->present()->fullName(),
|
||||
'target_id' => $user,
|
||||
'target_type' => User::class,
|
||||
'created_at' => $checkindate,
|
||||
'action_type' => 'checkin'
|
||||
));
|
||||
}
|
||||
|
||||
$status['success'][]['asset'][$asset_tag]['msg'] = 'Asset successfully matched for ' . $record['Full Name'] . ' on ' . $checkoutdate;
|
||||
}
|
||||
else {
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Checkin date needs to be after checkout date.';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$status['error'][]['asset'][$asset_tag]['msg'] = 'Asset not found in Snipe';
|
||||
}
|
||||
}
|
||||
|
||||
return view('hardware/history')->with('status', $status);
|
||||
}
|
||||
|
||||
protected function sortByName(array $recordA, array $recordB): int
|
||||
{
|
||||
return strcmp($recordB['Full Name'], $recordA['Full Name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retore a deleted asset.
|
||||
*
|
||||
|
|
|
@ -49,171 +49,114 @@
|
|||
<div class="box box-default">
|
||||
<div class="box-body">
|
||||
<div class="col-md-12">
|
||||
<form class="form-horizontal" role="form" method="post" enctype="multipart/form-data" action="">
|
||||
<!-- CSRF Token -->
|
||||
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
|
||||
<form class="form-horizontal" role="form" method="post" enctype="multipart/form-data" action="">
|
||||
<!-- CSRF Token -->
|
||||
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
|
||||
|
||||
@if (Session::get('message'))
|
||||
<p class="alert-danger">
|
||||
You have an error in your CSV file:<br />
|
||||
{{ Session::get('message') }}
|
||||
@if (Session::get('message'))
|
||||
<p class="alert-danger">
|
||||
You have an error in your CSV file:<br />
|
||||
{{ Session::get('message') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<p>
|
||||
Use this tool to import asset history you may have in CSV format. This information likely will be extracted from your previous asset management system.
|
||||
</p>
|
||||
<p>
|
||||
<i>Asset history</i> is defined as a checkout and subsequent checkin that has happened in the past. The assets and users MUST already exist in SNIPE, or they will be skipped. Matching assets for history import happens against the asset tag. We will try to find a matching user based on the user's full name you provide, based on the username format you configured in the Admin > General Settings.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<p>
|
||||
Upload a CSV that contains asset history. The assets and users MUST already exist in the system, or they will be skipped. Matching assets for history import happens against the asset tag. We will try to find a matching user based on the user's name you provide, and the criteria you select below. If you do not select any criteria below, it will simply try to match on the username format you configured in the Admin > General Settings.
|
||||
</p>
|
||||
<p>
|
||||
Fields included in the CSV must match <i>exactly</i> these header values: <strong>Asset Tag, Checkout Date, Checkin Date, Full Name</strong>. Any additional fields will be ignored.
|
||||
</p>
|
||||
|
||||
<p>Fields included in the CSV must match the headers: <strong>Date, Tag, Name</strong>. Any additional fields will be ignored. </p>
|
||||
|
||||
<p><strong>Date</strong> should be the checkout date. <strong>Tag</strong> should be the asset tag. <strong>Name</strong> should be the user's name (firstname lastname).</p>
|
||||
|
||||
<p><strong>History should be ordered by date in ascending order.</strong></p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="first_name" class="col-sm-3 control-label">{{ trans('admin/users/general.usercsv') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="file" name="user_import_csv" id="user_import_csv"{{ (config('app.lock_passwords')===true) ? ' disabled' : '' }}>
|
||||
<div class="form-group">
|
||||
<label for="first_name" class="col-sm-3 control-label">{{ trans('admin/users/general.usercsv') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="file" name="user_import_csv" id="user_import_csv"{{ (config('app.lock_passwords')===true) ? ' disabled' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="box-footer text-right">
|
||||
@if (config('app.lock_passwords')===true)
|
||||
<div class="col-md-12">
|
||||
<div class="callout callout-info">
|
||||
{{ trans('general.feature_disabled') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Match firstname.lastname -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
{{ Form::checkbox('match_firstnamelastname', '1', Input::old('match_firstnamelastname')) }} Try to match users by firstname.lastname (jane.smith) format
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Match flastname -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
{{ Form::checkbox('match_flastname', '1', Input::old('match_flastname')) }} Try to match users by first initial last name (jsmith) format
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Match firstname -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
{{ Form::checkbox('match_firstname', '1', Input::old('match_firstname')) }} Try to match users by first name (jane) format
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Match email -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
{{ Form::checkbox('match_email', '1', Input::old('match_email')) }} Try to match users by email as username
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="box-footer text-right">
|
||||
@if (config('app.lock_passwords')===true)
|
||||
<div class="col-md-12">
|
||||
<div class="callout callout-info">
|
||||
{{ trans('general.feature_disabled') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<button type="submit" class="btn btn-default">{{ trans('button.submit') }}</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@if (isset($status))
|
||||
|
||||
|
||||
@if (count($status['error']) > 0)
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"> {{ count($status['error']) }} Error Messages </h3>
|
||||
@else
|
||||
<button type="submit" class="btn btn-default">{{ trans('button.submit') }}</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div style="height : 400px; overflow : auto;">
|
||||
<table class="table">
|
||||
@for ($x = 0; $x < count($status['error']); $x++)
|
||||
@foreach($status['error'][$x] as $object_type => $message)
|
||||
<tr class="danger">
|
||||
<td><strong>{{ ucwords($object_type) }} {{ key($message) }}:</strong></td>
|
||||
<td>{{ $message[key($message)]['msg'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</form>
|
||||
|
||||
@if (count($status['success']) > 0)
|
||||
|
||||
@if (isset($status))
|
||||
|
||||
|
||||
@if (count($status['error']) > 0)
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"> {{ count($status['success']) }} Success Messages </h3>
|
||||
<h3 class="box-title"> {{ count($status['error']) }} Error Messages </h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div style="height : 400px; overflow : auto;">
|
||||
<table class="table">
|
||||
@for ($x = 0; $x < count($status['success']); $x++)
|
||||
@foreach($status['success'][$x] as $object_type => $message)
|
||||
<tr class="success">
|
||||
<td><strong>{{ ucwords($object_type) }} {{ key($message) }}:</strong></td>
|
||||
<td>{{ $message[key($message)]['msg'] }}</td>
|
||||
</tr>
|
||||
@for ($x = 0; $x < count($status['error']); $x++)
|
||||
@foreach($status['error'][$x] as $object_type => $message)
|
||||
<tr class="danger">
|
||||
<td><strong>{{ ucwords($object_type) }} {{ key($message) }}:</strong></td>
|
||||
<td>{{ $message[key($message)]['msg'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
</div></div></div>
|
||||
<script nonce="{{ csrf_token() }}">
|
||||
$(document).ready(function(){
|
||||
@endif
|
||||
|
||||
$('#generate-password').pGenerator({
|
||||
'bind': 'click',
|
||||
'passwordElement': '#password',
|
||||
'displayElement': '#password-display',
|
||||
'passwordLength': 10,
|
||||
'uppercase': true,
|
||||
'lowercase': true,
|
||||
'numbers': true,
|
||||
'specialChars': false,
|
||||
@if (count($status['success']) > 0)
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-default">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title"> {{ count($status['success']) }} Success Messages </h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div style="height : 400px; overflow : auto;">
|
||||
<table class="table">
|
||||
@for ($x = 0; $x < count($status['success']); $x++)
|
||||
@foreach($status['success'][$x] as $object_type => $message)
|
||||
<tr class="success">
|
||||
<td><strong>{{ ucwords($object_type) }} {{ key($message) }}:</strong></td>
|
||||
<td>{{ $message[key($message)]['msg'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
});
|
||||
});
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
@stop
|
||||
@stop
|
||||
|
|
|
@ -442,6 +442,8 @@
|
|||
{{ trans('general.asset_maintenances') }}
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
@can('admin')
|
||||
<li>
|
||||
<a href="{{ url('hardware/history') }}">
|
||||
{{ trans('general.import-history') }}
|
||||
|
|
Loading…
Reference in a new issue