From 3f7d2aebc7b27e0006e27517a621ccaa1c1a7676 Mon Sep 17 00:00:00 2001 From: herroworrd <47008367+herroworrd@users.noreply.github.com> Date: Fri, 8 Feb 2019 16:05:56 -0800 Subject: [PATCH] Fixed #6634: Asset Import History fixes and optimizations (#6657) * 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 --- .../Controllers/Assets/AssetsController.php | 181 +++++++-------- resources/views/hardware/history.blade.php | 219 +++++++----------- resources/views/layouts/default.blade.php | 2 + 3 files changed, 162 insertions(+), 240 deletions(-) diff --git a/app/Http/Controllers/Assets/AssetsController.php b/app/Http/Controllers/Assets/AssetsController.php index 48d9adb888..3309044c9b 100755 --- a/app/Http/Controllers/Assets/AssetsController.php +++ b/app/Http/Controllers/Assets/AssetsController.php @@ -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] [] - * @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. * diff --git a/resources/views/hardware/history.blade.php b/resources/views/hardware/history.blade.php index 92f38a269c..d42395a7b3 100644 --- a/resources/views/hardware/history.blade.php +++ b/resources/views/hardware/history.blade.php @@ -49,171 +49,114 @@
-
- - + + + - @if (Session::get('message')) -

- You have an error in your CSV file:
- {{ Session::get('message') }} + @if (Session::get('message')) +

+ You have an error in your CSV file:
+ {{ Session::get('message') }} +

+ @endif + +

+ 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. +

+

+ Asset history 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.

- @endif -

- 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. -

+

+ Fields included in the CSV must match exactly these header values: Asset Tag, Checkout Date, Checkin Date, Full Name. Any additional fields will be ignored. +

-

Fields included in the CSV must match the headers: Date, Tag, Name. Any additional fields will be ignored.

- -

Date should be the checkout date. Tag should be the asset tag. Name should be the user's name (firstname lastname).

- -

History should be ordered by date in ascending order.

- -
- -
- +
+ +
+ +
-
+ + - - - -
- - - - -
- -
- - @if (isset($status)) - - - @if (count($status['error']) > 0) -
-
-
-
-

{{ count($status['error']) }} Error Messages

+ @else + + @endif
-
-
- - @for ($x = 0; $x < count($status['error']); $x++) - @foreach($status['error'][$x] as $object_type => $message) - - - - - @endforeach - @endfor -
{{ ucwords($object_type) }} {{ key($message) }}:{{ $message[key($message)]['msg'] }}
-
-
-
-
-
- @endif + - @if (count($status['success']) > 0) + + @if (isset($status)) + + + @if (count($status['error']) > 0)
-

{{ count($status['success']) }} Success Messages

+

{{ count($status['error']) }} Error Messages

- @for ($x = 0; $x < count($status['success']); $x++) - @foreach($status['success'][$x] as $object_type => $message) - - - - + @for ($x = 0; $x < count($status['error']); $x++) + @foreach($status['error'][$x] as $object_type => $message) + + + + @endforeach @endfor
{{ ucwords($object_type) }} {{ key($message) }}:{{ $message[key($message)]['msg'] }}
{{ ucwords($object_type) }} {{ key($message) }}:{{ $message[key($message)]['msg'] }}
-
+
- @endif - @endif -
- -@stop + @stop diff --git a/resources/views/layouts/default.blade.php b/resources/views/layouts/default.blade.php index d819020b83..20714d2de3 100644 --- a/resources/views/layouts/default.blade.php +++ b/resources/views/layouts/default.blade.php @@ -442,6 +442,8 @@ {{ trans('general.asset_maintenances') }} + @endcan + @can('admin')
  • {{ trans('general.import-history') }}