Importer2 checkout (#5771)

* Importer: checkout to location, backend changes+tests.

* Import location checkout. Frontend changes.

* Allow importing of item number/model number for consumables.
This commit is contained in:
Daniel Meltzer 2018-07-05 15:22:24 -04:00 committed by snipe
parent 311f9fcefb
commit 880faa83a6
15 changed files with 126 additions and 34 deletions

View file

@ -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. // We need to save the user if it exists so that we can checkout to user later.
// Sanitizing the item will remove it. // Sanitizing the item will remove it.
if(array_key_exists('user', $this->item)) { if(array_key_exists('checkout_target', $this->item)) {
$user = $this->item['user']; $target = $this->item['checkout_target'];
} }
$item = $this->sanitizeItemForStoring($asset, $editingAsset); $item = $this->sanitizeItemForStoring($asset, $editingAsset);
// The location id fetched by the csv reader is actually the rtd_location_id. // 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'); $asset->logCreate('Imported using csv importer');
$this->log('Asset ' . $this->item["name"] . ' with serial number ' . $this->item['serial'] . ' was created'); $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 we have a target to checkout to, lets do so.
if(isset($user)) { if(isset($target)) {
$asset->fresh()->checkOut($user); $asset->fresh()->checkOut($target);
} }
return; return;
} }

View file

@ -14,17 +14,18 @@ class ConsumableImporter extends ItemImporter
protected function handle($row) protected function handle($row)
{ {
parent::handle($row); // TODO: Change the autogenerated stub parent::handle($row);
$this->createConsumableIfNotExists(); $this->createConsumableIfNotExists($row);
} }
/** /**
* Create a consumable if a duplicate does not exist * Create a consumable if a duplicate does not exist
* *
* @author Daniel Melzter * @author Daniel Melzter
* @param array $row CSV Row Being parsed.
* @since 3.0 * @since 3.0
*/ */
public function createConsumableIfNotExists() public function createConsumableIfNotExists($row)
{ {
$consumable = Consumable::where('name', $this->item['name'])->first(); $consumable = Consumable::where('name', $this->item['name'])->first();
if ($consumable) { if ($consumable) {
@ -39,6 +40,8 @@ class ConsumableImporter extends ItemImporter
} }
$this->log("No matching consumable, creating one"); $this->log("No matching consumable, creating one");
$consumable = new Consumable(); $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)); $consumable->fill($this->sanitizeItemForStoring($consumable));
//FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything. //FIXME: this disables model validation. Need to find a way to avoid double-logs without breaking everything.
$consumable->unsetEventDispatcher(); $consumable->unsetEventDispatcher();

View file

@ -30,8 +30,11 @@ abstract class Importer
private $defaultFieldMap = [ private $defaultFieldMap = [
'asset_tag' => 'asset tag', 'asset_tag' => 'asset tag',
'category' => 'category', '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', 'company' => 'company',
'item_name' => 'item name', 'item_name' => 'item name',
'item_number' => "item number",
'image' => 'image', 'image' => 'image',
'expiration_date' => 'expiration date', 'expiration_date' => 'expiration date',
'location' => 'location', 'location' => 'location',
@ -89,11 +92,12 @@ abstract class Importer
public function __construct($file) public function __construct($file)
{ {
$this->fieldMap = $this->defaultFieldMap; $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")) { if (! ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1'); 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)) { if (is_file($file)) {
$this->csv = Reader::createFromPath($file); $this->csv = Reader::createFromPath($file);
} else { } else {
@ -179,9 +183,8 @@ abstract class Importer
*/ */
public function lookupCustomKey($key) public function lookupCustomKey($key)
{ {
// dd($this->fieldMap);
if (array_key_exists($key, $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]; return $this->fieldMap[$key];
} }
// Otherwise no custom key, return original. // 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 * @param $results
* @return array * @return array
*/ */
@ -249,11 +254,11 @@ abstract class Importer
$last_name = ''; $last_name = '';
if(empty($user_name) && empty($user_email) && empty($user_username)) { if(empty($user_name) && empty($user_email) && empty($user_username)) {
$this->log('No user data provided - skipping user creation, just adding asset'); $this->log('No user data provided - skipping user creation, just adding asset');
//$user_username = '';
return false; return false;
} }
// A username was given.
if( !empty($user_username)) { if( !empty($user_username)) {
// A username was given.
$user = User::where('username', $user_username)->first(); $user = User::where('username', $user_username)->first();
if($user) { if($user) {
return $user; return $user;

View file

@ -69,11 +69,36 @@ class ItemImporter extends Importer
$this->item['serial'] = $this->findCsvMatch($row, "serial"); $this->item['serial'] = $this->findCsvMatch($row, "serial");
// NO need to call this method if we're running the user import. // NO need to call this method if we're running the user import.
// TODO: Merge these methods. // TODO: Merge these methods.
$this->item['checkout_class'] = $this->findCsvMatch($row, "checkout_class");
if(get_class($this) !== UserImporter::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. * Cleanup the $item array before storing.
* We need to remove any values that are not part of the fillable fields. * We need to remove any values that are not part of the fillable fields.

View file

@ -71,11 +71,11 @@ class LicenseImporter extends ItemImporter
// Lets try to checkout seats if the fields exist and we have seats. // Lets try to checkout seats if the fields exist and we have seats.
if ($license->seats > 0) { if ($license->seats > 0) {
$user = $this->item['user']; $checkout_target = $this->item['checkout_target'];
$asset = Asset::where('asset_tag', $asset_tag)->first(); $asset = Asset::where('asset_tag', $asset_tag)->first();
$targetLicense = $license->licenseSeats()->first(); $targetLicense = $license->licenseSeats()->first();
if ($user) { if ($checkout_target) {
$targetLicense->assigned_to = $user->id; $targetLicense->assigned_to = $checkout_target->id;
if ($asset) { if ($asset) {
$targetLicense->asset_id = $asset->id; $targetLicense->asset_id = $asset->id;
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

View file

@ -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/AdminLTE.css": "/css/AdminLTE.css?id=5e72463a66acbcc740d5",
"/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405", "/css/app.css": "/css/app.css?id=407edb63cc6b6dc62405",
"/css/overrides.css": "/css/overrides.css?id=2d81c3704393bac77011", "/css/overrides.css": "/css/overrides.css?id=2d81c3704393bac77011",
"/js/build/vue.js.map": "/js/build/vue.js.map?id=e0eb0edc0b761965973f", "/js/build/vue.js.map": "/js/build/vue.js.map?id=43e461db5a50dbd76462",
"/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=99f5a5a03c4155cf69f6", "/css/AdminLTE.css.map": "/css/AdminLTE.css.map?id=0be7790b84909dca6a0a",
"/css/app.css.map": "/css/app.css.map?id=bdbe05e6ecd70ccfac72", "/css/app.css.map": "/css/app.css.map?id=96b5c985e860716e6a16",
"/css/overrides.css.map": "/css/overrides.css.map?id=898c91d4a425b01b589b", "/css/overrides.css.map": "/css/overrides.css.map?id=f7ce9ca49027594ac402",
"/css/dist/all.css": "/css/dist/all.css?id=98db4e9b7650453c8b00", "/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", "/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"
} }

View file

@ -117,12 +117,18 @@ tr {
assets: [ assets: [
{id: 'asset_tag', text: 'Asset Tag' }, {id: 'asset_tag', text: 'Asset Tag' },
{id: 'asset_model', text: 'Model Name' }, {id: 'asset_model', text: 'Model Name' },
{id: 'checkout_class', text: 'Checkout Type' },
{id: 'checkout_location', text: 'Checkout Location' },
{id: 'image', text: 'Image Filename' }, {id: 'image', text: 'Image Filename' },
{id: 'model_number', text: 'Model Number' }, {id: 'model_number', text: 'Model Number' },
{id: 'full_name', text: 'Full Name' }, {id: 'full_name', text: 'Full Name' },
{id: 'status', text: 'Status' }, {id: 'status', text: 'Status' },
{id: 'warranty_months', text: 'Warranty Months' }, {id: 'warranty_months', text: 'Warranty Months' },
], ],
consumables: [
{id: 'item_no', text: "Item Number"},
{id: 'model_number', text: "Model Number"},
],
licenses: [ licenses: [
{id: 'expiration_date', text: 'Expiration Date' }, {id: 'expiration_date', text: 'Expiration Date' },
{id: 'license_email', text: 'Licensed To Email' }, {id: 'license_email', text: 'Licensed To Email' },
@ -165,6 +171,11 @@ tr {
.concat(this.columnOptions.assets) .concat(this.columnOptions.assets)
.concat(this.columnOptions.customFields) .concat(this.columnOptions.customFields)
.sort(sorter); .sort(sorter);
case 'consumable':
return this.columnOptions.general
.concat(this.columnOptions.consumables)
.sort(sorter);
case 'license': case 'license':
return this.columnOptions.general.concat(this.columnOptions.licenses).sort(sorter); return this.columnOptions.general.concat(this.columnOptions.licenses).sort(sorter);
case 'user': case 'user':
@ -184,7 +195,6 @@ tr {
}, },
watch: { watch: {
columns() { columns() {
console.log("CHANGED");
this.populateSelect2ActiveItems(); this.populateSelect2ActiveItems();
} }
}, },
@ -234,7 +244,6 @@ tr {
for(var j=0; j < this.columns.length; j++) { for(var j=0; j < this.columns.length; j++) {
let column = this.columns[j]; let column = this.columns[j];
let lower = this.file.header_row.map((value) => value.toLowerCase()); let lower = this.file.header_row.map((value) => value.toLowerCase());
console.dir(lower);
let index = lower.indexOf(column.text.toLowerCase()) let index = lower.indexOf(column.text.toLowerCase())
if(index != -1) { if(index != -1) {
this.$set(this.columnMappings, this.file.header_row[index], column.id) this.$set(this.columnMappings, this.file.header_row[index], column.id)
@ -248,7 +257,6 @@ tr {
} }
}, },
updateModel(header, value) { updateModel(header, value) {
console.log(header, value);
this.columnMappings[header] = value; this.columnMappings[header] = value;
} }
}, },

View file

@ -8,6 +8,7 @@ use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Category; use App\Models\Category;
use App\Models\CustomField; use App\Models\CustomField;
use App\Models\Location;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -72,12 +73,61 @@ EOT;
'asset_tag' => '970882174-8', '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.", '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_date' => '2016-04-05 00:00:01',
'purchase_cost' => 133289.59, 'purchase_cost' => 133289.59
'warranty_months' => 14, , 'warranty_months' => 14,
'_snipeit_weight_2' => 35 '_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() public function testUpdateAssetIncludingCustomFields()
{ {
$this->signIn(); $this->signIn();
@ -385,18 +435,19 @@ EOT;
public function testDefaultConsumableImport() public function testDefaultConsumableImport()
{ {
$csv = <<<'EOT' $csv = <<<'EOT'
Item Name,Purchase Date,Purchase Cost,Location,Company,Order Number,Category,Requestable,Quantity 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 eget,01/03/2011,$85.91,mauris blandit mattis.,Lycos,T295T06V,Triamterene/Hydrochlorothiazide,No,322,3305,30123
EOT; EOT;
$this->import(new ConsumableImporter($csv)); $this->import(new ConsumableImporter($csv));
$this->tester->seeRecord('consumables', [ $this->tester->seeRecord('consumables', [
'name' => 'eget', 'name' => 'eget',
'purchase_date' => '2011-01-03 00:00:01', 'purchase_date' => '2011-01-03 00:00:01',
'purchase_cost' => 85.91, 'purchase_cost' => 85.91,
'order_number' => 'T295T06V', 'order_number' => 'T295T06V',
'requestable' => 0, 'requestable' => 0,
'qty' => 322 'qty' => 322,
'item_no' => 3305,
'model_number' => 30123
]); ]);
$this->tester->seeRecord('locations', [ $this->tester->seeRecord('locations', [