diff --git a/app/Importer/AssetImporter.php b/app/Importer/AssetImporter.php index a8fd712536..e0977eac6c 100644 --- a/app/Importer/AssetImporter.php +++ b/app/Importer/AssetImporter.php @@ -81,8 +81,8 @@ class AssetImporter extends ItemImporter // We need to save the user if it exists so that we can checkout to user later. // Sanitizing the item will remove it. - if(array_key_exists('user', $this->item)) { - $user = $this->item['user']; + if(array_key_exists('checkout_target', $this->item)) { + $target = $this->item['checkout_target']; } $item = $this->sanitizeItemForStoring($asset, $editingAsset); // The location id fetched by the csv reader is actually the rtd_location_id. @@ -112,9 +112,9 @@ class AssetImporter extends ItemImporter $asset->logCreate('Imported using csv importer'); $this->log('Asset ' . $this->item["name"] . ' with serial number ' . $this->item['serial'] . ' was created'); - // If we have a user to checkout to, lets do so. - if(isset($user)) { - $asset->fresh()->checkOut($user); + // If we have a target to checkout to, lets do so. + if(isset($target)) { + $asset->fresh()->checkOut($target); } return; } diff --git a/app/Importer/ConsumableImporter.php b/app/Importer/ConsumableImporter.php index f81b575805..a25a8b56f7 100644 --- a/app/Importer/ConsumableImporter.php +++ b/app/Importer/ConsumableImporter.php @@ -14,17 +14,18 @@ class ConsumableImporter extends ItemImporter protected function handle($row) { - parent::handle($row); // TODO: Change the autogenerated stub - $this->createConsumableIfNotExists(); + parent::handle($row); + $this->createConsumableIfNotExists($row); } /** * Create a consumable if a duplicate does not exist * * @author Daniel Melzter + * @param array $row CSV Row Being parsed. * @since 3.0 */ - public function createConsumableIfNotExists() + public function createConsumableIfNotExists($row) { $consumable = Consumable::where('name', $this->item['name'])->first(); if ($consumable) { @@ -39,6 +40,8 @@ class ConsumableImporter extends ItemImporter } $this->log("No matching consumable, creating one"); $consumable = new Consumable(); + $this->item['model_number'] = $this->findCsvMatch($row, "model_number");; + $this->item['item_no'] = $this->findCsvMatch($row, "item_number"); $consumable->fill($this->sanitizeItemForStoring($consumable)); //FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything. $consumable->unsetEventDispatcher(); diff --git a/app/Importer/Importer.php b/app/Importer/Importer.php index db7dc938f0..1ec04157ba 100644 --- a/app/Importer/Importer.php +++ b/app/Importer/Importer.php @@ -30,8 +30,11 @@ abstract class Importer private $defaultFieldMap = [ 'asset_tag' => 'asset tag', 'category' => 'category', + 'checkout_class' => 'checkout type', // Supports Location or User for assets. Using checkout_class instead of checkout_type because type exists on asset already. + 'checkout_location' => 'checkout location', 'company' => 'company', 'item_name' => 'item name', + 'item_number' => "item number", 'image' => 'image', 'expiration_date' => 'expiration date', 'location' => 'location', @@ -89,11 +92,12 @@ abstract class Importer public function __construct($file) { $this->fieldMap = $this->defaultFieldMap; - // By default the importer passes a url to the file. - // However, for testing we also support passing a string directly if (! ini_get("auto_detect_line_endings")) { ini_set("auto_detect_line_endings", '1'); } + + // By default the importer passes a url to the file. + // However, for testing we also support passing a string directly if (is_file($file)) { $this->csv = Reader::createFromPath($file); } else { @@ -179,9 +183,8 @@ abstract class Importer */ public function lookupCustomKey($key) { - // dd($this->fieldMap); if (array_key_exists($key, $this->fieldMap)) { - $this->log("Found a match in our custom map: {$key} is " . $this->fieldMap[$key]); + // $this->log("Found a match in our custom map: {$key} is " . $this->fieldMap[$key]); return $this->fieldMap[$key]; } // Otherwise no custom key, return original. @@ -189,6 +192,8 @@ abstract class Importer } /** + * Used to lowercase header values to ensure we're comparing values properly. + * * @param $results * @return array */ @@ -249,11 +254,11 @@ abstract class Importer $last_name = ''; if(empty($user_name) && empty($user_email) && empty($user_username)) { $this->log('No user data provided - skipping user creation, just adding asset'); - //$user_username = ''; return false; } - // A username was given. + if( !empty($user_username)) { + // A username was given. $user = User::where('username', $user_username)->first(); if($user) { return $user; diff --git a/app/Importer/ItemImporter.php b/app/Importer/ItemImporter.php index 72a618cbd1..5877ad81dd 100644 --- a/app/Importer/ItemImporter.php +++ b/app/Importer/ItemImporter.php @@ -69,11 +69,36 @@ class ItemImporter extends Importer $this->item['serial'] = $this->findCsvMatch($row, "serial"); // NO need to call this method if we're running the user import. // TODO: Merge these methods. + $this->item['checkout_class'] = $this->findCsvMatch($row, "checkout_class"); if(get_class($this) !== UserImporter::class) { - $this->item["user"] = $this->createOrFetchUser($row); + // $this->item["user"] = $this->createOrFetchUser($row); + $this->item["checkout_target"] = $this->determineCheckout($row); + } } + /** + * Parse row to determine what (if anything) we should checkout to. + * @param array $row CSV Row being parsed + * @return SnipeModel Model to be checked out to + */ + protected function determineCheckout($row) + { + // We only supporty checkout-to-location for asset, so short circuit otherw. + if(get_class($this) != AssetImporter::class) { + return $this->createOrFetchUser($row); + } + + if ($this->item['checkout_class'] === 'location') { + // dd($this->findCsvMatch($row, 'checkout_location')); + return Location::findOrFail($this->createOrFetchLocation($this->findCsvMatch($row, 'checkout_location'))); + // dd('here'); + } + + return $this->createOrFetchUser($row); + + } + /** * Cleanup the $item array before storing. * We need to remove any values that are not part of the fillable fields. diff --git a/app/Importer/LicenseImporter.php b/app/Importer/LicenseImporter.php index dd554bf61d..7be62b7b44 100644 --- a/app/Importer/LicenseImporter.php +++ b/app/Importer/LicenseImporter.php @@ -71,11 +71,11 @@ class LicenseImporter extends ItemImporter // Lets try to checkout seats if the fields exist and we have seats. if ($license->seats > 0) { - $user = $this->item['user']; + $checkout_target = $this->item['checkout_target']; $asset = Asset::where('asset_tag', $asset_tag)->first(); $targetLicense = $license->licenseSeats()->first(); - if ($user) { - $targetLicense->assigned_to = $user->id; + if ($checkout_target) { + $targetLicense->assigned_to = $checkout_target->id; if ($asset) { $targetLicense->asset_id = $asset->id; } diff --git a/public/css/AdminLTE.css.map b/public/css/AdminLTE.css.map index 8edbdcc79f..311c875fa8 100644 Binary files a/public/css/AdminLTE.css.map and b/public/css/AdminLTE.css.map differ diff --git a/public/css/app.css.map b/public/css/app.css.map index 95ae4d6a82..051d67395d 100644 Binary files a/public/css/app.css.map and b/public/css/app.css.map differ diff --git a/public/css/overrides.css.map b/public/css/overrides.css.map index 7c74906ef6..dfc414f418 100644 Binary files a/public/css/overrides.css.map and b/public/css/overrides.css.map differ diff --git a/public/js/build/all.js b/public/js/build/all.js index 3767ea093f..9bcfff3ecc 100644 Binary files a/public/js/build/all.js and b/public/js/build/all.js differ diff --git a/public/js/build/vue.js b/public/js/build/vue.js index 79892227b7..7a25f73905 100644 Binary files a/public/js/build/vue.js and b/public/js/build/vue.js differ diff --git a/public/js/build/vue.js.map b/public/js/build/vue.js.map index c5105e8637..947c9297f9 100644 Binary files a/public/js/build/vue.js.map and b/public/js/build/vue.js.map differ diff --git a/public/js/dist/all.js b/public/js/dist/all.js index 3767ea093f..9bcfff3ecc 100644 Binary files a/public/js/dist/all.js and b/public/js/dist/all.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index af31e652c9..faf3534f11 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,14 +1,14 @@ { - "/js/build/vue.js": "/js/build/vue.js?id=992bcb968a7f2da998b5", + "/js/build/vue.js": "/js/build/vue.js?id=886005d54e9097be1aa2", "/css/AdminLTE.css": "/css/AdminLTE.css?id=5e72463a66acbcc740d5", "/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405", "/css/overrides.css": "/css/overrides.css?id=2d81c3704393bac77011", - "/js/build/vue.js.map": "/js/build/vue.js.map?id=e0eb0edc0b761965973f", - "/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=99f5a5a03c4155cf69f6", - "/css/app.css.map": "/css/app.css.map?id=bdbe05e6ecd70ccfac72", - "/css/overrides.css.map": "/css/overrides.css.map?id=898c91d4a425b01b589b", + "/js/build/vue.js.map": "/js/build/vue.js.map?id=43e461db5a50dbd76462", + "/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=0be7790b84909dca6a0a", + "/css/app.css.map": "/css/app.css.map?id=96b5c985e860716e6a16", + "/css/overrides.css.map": "/css/overrides.css.map?id=f7ce9ca49027594ac402", "/css/dist/all.css": "/css/dist/all.css?id=98db4e9b7650453c8b00", - "/js/dist/all.js": "/js/dist/all.js?id=39b95992f478d68c44a8", + "/js/dist/all.js": "/js/dist/all.js?id=2d0469b07a6bc188bedd", "/css/build/all.css": "/css/build/all.css?id=98db4e9b7650453c8b00", - "/js/build/all.js": "/js/build/all.js?id=39b95992f478d68c44a8" + "/js/build/all.js": "/js/build/all.js?id=2d0469b07a6bc188bedd" } \ No newline at end of file diff --git a/resources/assets/js/components/importer/importer-file.vue b/resources/assets/js/components/importer/importer-file.vue index 3014a44c06..7c8dbae002 100644 --- a/resources/assets/js/components/importer/importer-file.vue +++ b/resources/assets/js/components/importer/importer-file.vue @@ -117,12 +117,18 @@ tr { assets: [ {id: 'asset_tag', text: 'Asset Tag' }, {id: 'asset_model', text: 'Model Name' }, + {id: 'checkout_class', text: 'Checkout Type' }, + {id: 'checkout_location', text: 'Checkout Location' }, {id: 'image', text: 'Image Filename' }, {id: 'model_number', text: 'Model Number' }, {id: 'full_name', text: 'Full Name' }, {id: 'status', text: 'Status' }, {id: 'warranty_months', text: 'Warranty Months' }, ], + consumables: [ + {id: 'item_no', text: "Item Number"}, + {id: 'model_number', text: "Model Number"}, + ], licenses: [ {id: 'expiration_date', text: 'Expiration Date' }, {id: 'license_email', text: 'Licensed To Email' }, @@ -165,6 +171,11 @@ tr { .concat(this.columnOptions.assets) .concat(this.columnOptions.customFields) .sort(sorter); + + case 'consumable': + return this.columnOptions.general + .concat(this.columnOptions.consumables) + .sort(sorter); case 'license': return this.columnOptions.general.concat(this.columnOptions.licenses).sort(sorter); case 'user': @@ -184,7 +195,6 @@ tr { }, watch: { columns() { - console.log("CHANGED"); this.populateSelect2ActiveItems(); } }, @@ -234,7 +244,6 @@ tr { for(var j=0; j < this.columns.length; j++) { let column = this.columns[j]; let lower = this.file.header_row.map((value) => value.toLowerCase()); - console.dir(lower); let index = lower.indexOf(column.text.toLowerCase()) if(index != -1) { this.$set(this.columnMappings, this.file.header_row[index], column.id) @@ -248,7 +257,6 @@ tr { } }, updateModel(header, value) { - console.log(header, value); this.columnMappings[header] = value; } }, diff --git a/tests/unit/ImporterTest.php b/tests/unit/ImporterTest.php index 99469a8ecd..f996503962 100644 --- a/tests/unit/ImporterTest.php +++ b/tests/unit/ImporterTest.php @@ -8,6 +8,7 @@ use App\Models\Asset; use App\Models\AssetModel; use App\Models\Category; use App\Models\CustomField; +use App\Models\Location; use App\Models\User; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -72,12 +73,61 @@ EOT; 'asset_tag' => '970882174-8', 'notes' => "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est.", 'purchase_date' => '2016-04-05 00:00:01', - 'purchase_cost' => 133289.59, - 'warranty_months' => 14, + 'purchase_cost' => 133289.59 +, 'warranty_months' => 14, '_snipeit_weight_2' => 35 ]); } + public function testImportCheckoutToLocation() + { + $this->signIn(); + + // Testing in order: + // * Asset to user, no checkout type defined (default to user). + // * Asset to user, explicit user checkout type (Checkout to user) + // * Asset to location, location does not exist to begin with + // * Asset to preexisting location. + $csv = <<<'EOT' +Full Name,Email,Username,Checkout Location,Checkout Type,item Name,Category,Model name,Manufacturer,Model Number,Serial,Asset Tag,Location,Notes,Purchase Date,Purchase Cost,Company,Status,Warranty,Supplier,Weight +Bonnie Nelson,bnelson0@cdbaby.com,bnelson0,,,eget nunc donec quis,quam,massa id,Linkbridge,6377018600094472,27aa8378-b0f4-4289-84a4-405da95c6147,970882174-8,Daping,"Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est.",2016-04-05,133289.59,Alpha,Undeployable,14,Blogspan,35 +Mildred Gibson,mgibson2@wiley.com,mgibson2,,user,morbi quis tortor id,nunc nisl duis,convallis tortor risus,Lajo,374622546776765,2837ab20-8f0d-4935-8a52-226392f2b1b0,710141467-2,Shekou,In congue. Etiam justo. Etiam pretium iaculis justo.,2015-08-09,233.57,Konklab,Lost,,, +,,,Planet Earth,location,dictumst maecenas ut,sem praesent,accumsan felis,Layo,30052522651756,4751495c-cee0-4961-b788-94a545b5643e,998233705-X,Dante Delgado,,2016-04-16,261.79,,Archived,15,Ntag, +,,,Daping,location,viverra diam vitae,semper sapien,dapibus dolor vel,Flashset,3559785746335392,e287bb64-ff4f-434c-88ab-210ad433c77b,927820758-6,Achiaman,,2016-03-05,675.3,,Archived,22,Meevee, +EOT; + + $this->import(new AssetImporter($csv)); + $user = User::where('username', 'bnelson0')->firstOrFail(); + + $this->tester->seeRecord('assets', [ + 'asset_tag' => '970882174-8', + 'assigned_type' => User::class, + 'assigned_to' => $user->id + ]); + + $user = User::where('username', 'mgibson2')->firstOrFail(); + $this->tester->seeRecord('assets', [ + 'asset_tag' => '710141467-2', + 'assigned_type' => User::class, + 'assigned_to' => $user->id + ]); + + $location = Location::where('name', 'Planet Earth')->firstOrFail(); + $this->tester->seeRecord('assets', [ + 'asset_tag' => '998233705-X', + 'assigned_type' => Location::class, + 'assigned_to' => $location->id + ]); + + $location = Location::where('name', 'Daping')->firstOrFail(); + $this->tester->seeRecord('assets', [ + 'asset_tag' => '927820758-6', + 'assigned_type' => Location::class, + 'assigned_to' => $location->id + ]); + + } + public function testUpdateAssetIncludingCustomFields() { $this->signIn(); @@ -385,18 +435,19 @@ EOT; public function testDefaultConsumableImport() { $csv = <<<'EOT' -Item Name,Purchase Date,Purchase Cost,Location,Company,Order Number,Category,Requestable,Quantity -eget,01/03/2011,$85.91,mauris blandit mattis.,Lycos,T295T06V,Triamterene/Hydrochlorothiazide,No,322 +Item Name,Purchase Date,Purchase Cost,Location,Company,Order Number,Category,Requestable,Quantity,Item Number,Model Number +eget,01/03/2011,$85.91,mauris blandit mattis.,Lycos,T295T06V,Triamterene/Hydrochlorothiazide,No,322,3305,30123 EOT; $this->import(new ConsumableImporter($csv)); - $this->tester->seeRecord('consumables', [ 'name' => 'eget', 'purchase_date' => '2011-01-03 00:00:01', 'purchase_cost' => 85.91, 'order_number' => 'T295T06V', 'requestable' => 0, - 'qty' => 322 + 'qty' => 322, + 'item_no' => 3305, + 'model_number' => 30123 ]); $this->tester->seeRecord('locations', [