mirror of
https://github.com/snipe/snipe-it.git
synced 2025-01-01 00:47:27 -08:00
Merge pull request #15802 from snipe/features/import_models
Ability to import asset models (separate from assets)
This commit is contained in:
commit
3c08760aeb
|
@ -59,6 +59,7 @@ class AssetModelsController extends Controller
|
|||
'model_number',
|
||||
'min_amt',
|
||||
'eol',
|
||||
'created_by',
|
||||
'requestable',
|
||||
'models.notes',
|
||||
'models.created_at',
|
||||
|
@ -69,7 +70,7 @@ class AssetModelsController extends Controller
|
|||
'models.deleted_at',
|
||||
'models.updated_at',
|
||||
])
|
||||
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues','adminuser')
|
||||
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
|
||||
->withCount('assets as assets_count');
|
||||
|
||||
if ($request->input('status')=='deleted') {
|
||||
|
@ -95,7 +96,7 @@ class AssetModelsController extends Controller
|
|||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'models.created_at';
|
||||
|
||||
switch ($sort) {
|
||||
switch ($request->input('sort')) {
|
||||
case 'manufacturer':
|
||||
$assetmodels->OrderManufacturer($order);
|
||||
break;
|
||||
|
@ -105,6 +106,9 @@ class AssetModelsController extends Controller
|
|||
case 'fieldset':
|
||||
$assetmodels->OrderFieldset($order);
|
||||
break;
|
||||
case 'created_by':
|
||||
$assetmodels->OrderByCreatedByName($order);
|
||||
break;
|
||||
default:
|
||||
$assetmodels->orderBy($sort, $order);
|
||||
break;
|
||||
|
|
|
@ -28,8 +28,7 @@ class ImportController extends Controller
|
|||
public function index() : JsonResponse | array
|
||||
{
|
||||
$this->authorize('import');
|
||||
$imports = Import::latest()->get();
|
||||
|
||||
$imports = Import::with('adminuser')->latest()->get();
|
||||
return (new ImportsTransformer)->transformImports($imports);
|
||||
}
|
||||
|
||||
|
@ -133,7 +132,7 @@ class ImportController extends Controller
|
|||
}
|
||||
|
||||
$import->filesize = filesize($path.'/'.$file_name);
|
||||
|
||||
$import->created_by = auth()->id();
|
||||
$import->save();
|
||||
$results[] = $import;
|
||||
}
|
||||
|
@ -177,6 +176,9 @@ class ImportController extends Controller
|
|||
case 'asset':
|
||||
$redirectTo = 'hardware.index';
|
||||
break;
|
||||
case 'assetModel':
|
||||
$redirectTo = 'models.index';
|
||||
break;
|
||||
case 'accessory':
|
||||
$redirectTo = 'accessories.index';
|
||||
break;
|
||||
|
|
|
@ -38,10 +38,11 @@ class ItemImportRequest extends FormRequest
|
|||
|
||||
$filename = config('app.private_uploads').'/imports/'.$import->file_path;
|
||||
$import->import_type = $this->input('import-type');
|
||||
$class = title_case($import->import_type);
|
||||
$class = ucfirst($import->import_type);
|
||||
$classString = "App\\Importer\\{$class}Importer";
|
||||
$importer = new $classString($filename);
|
||||
$import->field_map = request('column-mappings');
|
||||
$import->created_by = auth()->id();
|
||||
$import->save();
|
||||
$fieldMappings = [];
|
||||
|
||||
|
@ -60,7 +61,7 @@ class ItemImportRequest extends FormRequest
|
|||
$fieldMappings = array_change_key_case(array_flip($import->field_map), CASE_LOWER);
|
||||
}
|
||||
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
|
||||
->setUserId(auth()->id())
|
||||
->setCreatedBy(auth()->id())
|
||||
->setUpdating($this->get('import-update'))
|
||||
->setShouldNotify($this->get('send-welcome'))
|
||||
->setUsernameFormat('firstname.lastname')
|
||||
|
|
|
@ -65,6 +65,10 @@ class AssetModelsTransformer
|
|||
'eol' => ($assetmodel->eol > 0) ? $assetmodel->eol.' months' : 'None',
|
||||
'requestable' => ($assetmodel->requestable == '1') ? true : false,
|
||||
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
|
||||
'created_by' => ($assetmodel->adminuser) ? [
|
||||
'id' => (int) $assetmodel->adminuser->id,
|
||||
'name'=> e($assetmodel->adminuser->present()->fullName()),
|
||||
] : null,
|
||||
'created_at' => Helper::getFormattedDateObject($assetmodel->created_at, 'datetime'),
|
||||
'updated_at' => Helper::getFormattedDateObject($assetmodel->updated_at, 'datetime'),
|
||||
'deleted_at' => Helper::getFormattedDateObject($assetmodel->deleted_at, 'datetime'),
|
||||
|
|
|
@ -42,6 +42,7 @@ class AccessoryImporter extends ItemImporter
|
|||
}
|
||||
$this->log('No Matching Accessory, Creating a new one');
|
||||
$accessory = new Accessory();
|
||||
$accessory->created_by = auth()->id();
|
||||
$this->item['model_number'] = $this->findCsvMatch($row, "model_number");
|
||||
$this->item['min_amt'] = $this->findCsvMatch($row, "min_amt");
|
||||
$accessory->fill($this->sanitizeItemForStoring($accessory));
|
||||
|
|
174
app/Importer/AssetModelImporter.php
Normal file
174
app/Importer/AssetModelImporter.php
Normal file
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
namespace App\Importer;
|
||||
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Depreciation;
|
||||
use App\Models\CustomFieldset;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* When we are importing users via an Asset/etc import, we use createOrFetchUser() in
|
||||
* Importer\Importer.php. [ALG]
|
||||
*
|
||||
* Class LocationImporter
|
||||
*/
|
||||
class AssetModelImporter extends ItemImporter
|
||||
{
|
||||
protected $models;
|
||||
|
||||
public function __construct($filename)
|
||||
{
|
||||
parent::__construct($filename);
|
||||
}
|
||||
|
||||
protected function handle($row)
|
||||
{
|
||||
parent::handle($row);
|
||||
$this->createAssetModelIfNotExists($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model if a duplicate does not exist.
|
||||
* @todo Investigate how this should interact with Importer::createModelIfNotExists
|
||||
*
|
||||
* @author A. Gianotto
|
||||
* @since 6.1.0
|
||||
* @param array $row
|
||||
*/
|
||||
public function createAssetModelIfNotExists(array $row)
|
||||
{
|
||||
|
||||
$editingAssetModel = false;
|
||||
$assetModel = AssetModel::where('name', '=', $this->findCsvMatch($row, 'name'))->first();
|
||||
|
||||
if ($assetModel) {
|
||||
if (! $this->updating) {
|
||||
$this->log('A matching Model '.$this->item['name'].' already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log('Updating Model');
|
||||
$editingAssetModel = true;
|
||||
} else {
|
||||
$this->log('No Matching Model, Create a new one');
|
||||
$assetModel = new AssetModel();
|
||||
}
|
||||
|
||||
// Pull the records from the CSV to determine their values
|
||||
$this->item['name'] = trim($this->findCsvMatch($row, 'name'));
|
||||
$this->item['category'] = trim($this->findCsvMatch($row, 'category'));
|
||||
$this->item['manufacturer'] = trim($this->findCsvMatch($row, 'manufacturer'));
|
||||
$this->item['min_amt'] = trim($this->findCsvMatch($row, 'min_amt'));
|
||||
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
||||
$this->item['eol'] = trim($this->findCsvMatch($row, 'eol'));
|
||||
$this->item['notes'] = trim($this->findCsvMatch($row, 'notes'));
|
||||
$this->item['fieldset'] = trim($this->findCsvMatch($row, 'fieldset'));
|
||||
$this->item['depreciation'] = trim($this->findCsvMatch($row, 'depreciation'));
|
||||
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? 1 : 0;
|
||||
|
||||
if (!empty($this->item['category'])) {
|
||||
if ($category = $this->createOrFetchCategory($this->item['category'])) {
|
||||
$this->item['category_id'] = $category;
|
||||
}
|
||||
}
|
||||
if (!empty($this->item['manufacturer'])) {
|
||||
if ($manufacturer = $this->createOrFetchManufacturer($this->item['manufacturer'])) {
|
||||
$this->item['manufacturer_id'] = $manufacturer;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->item['depreciation'])) {
|
||||
if ($depreciation = $this->fetchDepreciation($this->item['depreciation'])) {
|
||||
$this->item['depreciation_id'] = $depreciation;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->item['fieldset'])) {
|
||||
if ($fieldset = $this->createOrFetchCustomFieldset($this->item['fieldset'])) {
|
||||
$this->item['fieldset_id'] = $fieldset;
|
||||
}
|
||||
}
|
||||
|
||||
Log::debug('Item array is: ');
|
||||
Log::debug(print_r($this->item, true));
|
||||
|
||||
|
||||
if ($editingAssetModel) {
|
||||
Log::debug('Updating existing model');
|
||||
$assetModel->update($this->sanitizeItemForUpdating($assetModel));
|
||||
} else {
|
||||
Log::debug('Creating model');
|
||||
$assetModel->fill($this->sanitizeItemForStoring($assetModel));
|
||||
$assetModel->created_by = auth()->id();
|
||||
}
|
||||
|
||||
if ($assetModel->save()) {
|
||||
$this->log('AssetModel '.$assetModel->name.' created or updated from CSV import');
|
||||
return $assetModel;
|
||||
|
||||
} else {
|
||||
$this->log($assetModel->getErrors()->first());
|
||||
$this->addErrorToBag($assetModel, $assetModel->getErrors()->keys()[0], $assetModel->getErrors()->first());
|
||||
return $assetModel->getErrors();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch an existing depreciation, or create new if it doesn't exist.
|
||||
*
|
||||
* We only do a fetch vs create here since Depreciations have additional fields required
|
||||
* and cannot be created without them (months, for example.))
|
||||
*
|
||||
* @author A. Gianotto
|
||||
* @since 7.1.3
|
||||
* @param $depreciation_name string
|
||||
* @return int id of depreciation created/found
|
||||
*/
|
||||
public function fetchDepreciation($depreciation_name) : ?int
|
||||
{
|
||||
if ($depreciation_name != '') {
|
||||
|
||||
if ($depreciation = Depreciation::where('name', '=', $depreciation_name)->first()) {
|
||||
$this->log('A matching Depreciation '.$depreciation_name.' already exists');
|
||||
return $depreciation->id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an existing fieldset, or create new if it doesn't exist
|
||||
*
|
||||
* @author A. Gianotto
|
||||
* @since 7.1.3
|
||||
* @param $fieldset_name string
|
||||
* @return int id of fieldset created/found
|
||||
*/
|
||||
public function createOrFetchCustomFieldset($fieldset_name) : ?int
|
||||
{
|
||||
if ($fieldset_name != '') {
|
||||
$fieldset = CustomFieldset::where('name', '=', $fieldset_name)->first();
|
||||
|
||||
if ($fieldset) {
|
||||
$this->log('A matching fieldset '.$fieldset_name.' already exists');
|
||||
return $fieldset->id;
|
||||
}
|
||||
|
||||
$fieldset = new CustomFieldset();
|
||||
$fieldset->name = $fieldset_name;
|
||||
|
||||
if ($fieldset->save()) {
|
||||
$this->log('Fieldset '.$fieldset_name.' was created');
|
||||
|
||||
return $fieldset->id;
|
||||
}
|
||||
$this->logError($fieldset, 'Fieldset');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ class ComponentImporter extends ItemImporter
|
|||
}
|
||||
$this->log('No matching component, creating one');
|
||||
$component = new Component;
|
||||
$component->created_by = auth()->id();
|
||||
$component->fill($this->sanitizeItemForStoring($component));
|
||||
|
||||
// This sets an attribute on the Loggable trait for the action log
|
||||
|
@ -58,7 +59,7 @@ class ComponentImporter extends ItemImporter
|
|||
if (isset($this->item['asset_tag']) && ($asset = Asset::where('asset_tag', $this->item['asset_tag'])->first())) {
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_by' => $this->created_by,
|
||||
'created_by' => auth()->id(),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'assigned_qty' => 1, // Only assign the first one to the asset
|
||||
'asset_id' => $asset->id,
|
||||
|
|
|
@ -41,6 +41,7 @@ class ConsumableImporter extends ItemImporter
|
|||
}
|
||||
$this->log('No matching consumable, creating one');
|
||||
$consumable = new Consumable();
|
||||
$consumable->created_by = auth()->id();
|
||||
$this->item['model_number'] = trim($this->findCsvMatch($row, 'model_number'));
|
||||
$this->item['item_no'] = trim($this->findCsvMatch($row, 'item_number'));
|
||||
$this->item['min_amt'] = trim($this->findCsvMatch($row, "min_amt"));
|
||||
|
|
|
@ -363,6 +363,7 @@ abstract class Importer
|
|||
|
||||
// No luck finding a user on username or first name, let's create one.
|
||||
$user = new User;
|
||||
|
||||
$user->first_name = $user_array['first_name'];
|
||||
$user->last_name = $user_array['last_name'];
|
||||
$user->username = $user_array['username'];
|
||||
|
@ -406,7 +407,7 @@ abstract class Importer
|
|||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setUserId($created_by)
|
||||
public function setCreatedBy($created_by)
|
||||
{
|
||||
$this->created_by = $created_by;
|
||||
|
||||
|
@ -492,6 +493,16 @@ abstract class Importer
|
|||
|
||||
public function fetchHumanBoolean($value)
|
||||
{
|
||||
$true = [
|
||||
'yes',
|
||||
'y',
|
||||
'true',
|
||||
];
|
||||
|
||||
if (in_array(strtolower($value), $true)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
|
@ -528,6 +539,7 @@ abstract class Importer
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch an existing manager
|
||||
*
|
||||
|
|
|
@ -94,7 +94,7 @@ class ItemImporter extends Importer
|
|||
|
||||
$this->item['qty'] = $this->findCsvMatch($row, 'quantity');
|
||||
$this->item['requestable'] = $this->findCsvMatch($row, 'requestable');
|
||||
$this->item['created_by'] = $this->created_by;
|
||||
$this->item['created_by'] = auth()->id();
|
||||
$this->item['serial'] = $this->findCsvMatch($row, 'serial');
|
||||
// NO need to call this method if we're running the user import.
|
||||
// TODO: Merge these methods.
|
||||
|
@ -113,7 +113,7 @@ class ItemImporter extends Importer
|
|||
protected function determineCheckout($row)
|
||||
{
|
||||
// Locations don't get checked out to anyone/anything
|
||||
if (get_class($this) == LocationImporter::class) {
|
||||
if ((get_class($this) == LocationImporter::class) || (get_class($this) == AssetModelImporter::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -249,6 +249,7 @@ class ItemImporter extends Importer
|
|||
|
||||
$this->log('No Matching Model, Creating a new one');
|
||||
$asset_model = new AssetModel();
|
||||
$asset_model->created_by = auth()->id();
|
||||
$item = $this->sanitizeItemForStoring($asset_model, $editingModel);
|
||||
$item['name'] = $asset_model_name;
|
||||
$item['model_number'] = $asset_modelNumber;
|
||||
|
@ -256,11 +257,8 @@ class ItemImporter extends Importer
|
|||
$item['category_id'] = $this->createOrFetchCategory($asset_model_category);
|
||||
|
||||
$asset_model->fill($item);
|
||||
//$asset_model = AssetModel::firstOrNew($item);
|
||||
$item = null;
|
||||
|
||||
|
||||
|
||||
if ($asset_model->save()) {
|
||||
$this->log('Asset Model '.$asset_model_name.' with model number '.$asset_modelNumber.' was created');
|
||||
|
||||
|
@ -287,21 +285,28 @@ class ItemImporter extends Importer
|
|||
$classname = class_basename(get_class($this));
|
||||
$item_type = strtolower(substr($classname, 0, strpos($classname, 'Importer')));
|
||||
|
||||
// If we're importing asset models only (without attached assets), override the category type to asset
|
||||
if ($item_type == 'assetmodel') {
|
||||
$item_type = 'asset';
|
||||
}
|
||||
|
||||
if (empty($asset_category)) {
|
||||
$asset_category = 'Unnamed Category';
|
||||
}
|
||||
|
||||
|
||||
$category = Category::where(['name' => $asset_category, 'category_type' => $item_type])->first();
|
||||
|
||||
if ($category) {
|
||||
$this->log('A matching category: '.$asset_category.' already exists');
|
||||
|
||||
if ($category) {
|
||||
$this->log('A matching category: '.$category->name.' already exists');
|
||||
return $category->id;
|
||||
}
|
||||
|
||||
$category = new Category();
|
||||
$category->created_by = auth()->id();
|
||||
$category->name = $asset_category;
|
||||
$category->category_type = $item_type;
|
||||
$category->created_by = $this->created_by;
|
||||
|
||||
if ($category->save()) {
|
||||
$this->log('Category '.$asset_category.' was created');
|
||||
|
@ -330,6 +335,7 @@ class ItemImporter extends Importer
|
|||
return $company->id;
|
||||
}
|
||||
$company = new Company();
|
||||
$company->created_by = auth()->id();
|
||||
$company->name = $asset_company_name;
|
||||
|
||||
if ($company->save()) {
|
||||
|
@ -386,6 +392,7 @@ class ItemImporter extends Importer
|
|||
}
|
||||
$this->log('Creating a new status');
|
||||
$status = new Statuslabel();
|
||||
$status->created_by = auth()->id();
|
||||
$status->name = trim($asset_statuslabel_name);
|
||||
|
||||
$status->deployable = 1;
|
||||
|
@ -425,7 +432,7 @@ class ItemImporter extends Importer
|
|||
//Otherwise create a manufacturer.
|
||||
$manufacturer = new Manufacturer();
|
||||
$manufacturer->name = trim($item_manufacturer);
|
||||
$manufacturer->created_by = $this->created_by;
|
||||
$manufacturer->created_by = auth()->id();
|
||||
|
||||
if ($manufacturer->save()) {
|
||||
$this->log('Manufacturer '.$manufacturer->name.' was created');
|
||||
|
@ -466,7 +473,7 @@ class ItemImporter extends Importer
|
|||
$location->city = '';
|
||||
$location->state = '';
|
||||
$location->country = '';
|
||||
$location->created_by = $this->created_by;
|
||||
$location->created_by = auth()->id();
|
||||
|
||||
if ($location->save()) {
|
||||
$this->log('Location '.$asset_location.' was created');
|
||||
|
@ -502,7 +509,7 @@ class ItemImporter extends Importer
|
|||
|
||||
$supplier = new Supplier();
|
||||
$supplier->name = $item_supplier;
|
||||
$supplier->created_by = $this->created_by;
|
||||
$supplier->created_by = auth()->id();
|
||||
|
||||
if ($supplier->save()) {
|
||||
$this->log('Supplier '.$item_supplier.' was created');
|
||||
|
|
|
@ -84,6 +84,7 @@ class LicenseImporter extends ItemImporter
|
|||
$license->update($this->sanitizeItemForUpdating($license));
|
||||
} else {
|
||||
$license->fill($this->sanitizeItemForStoring($license));
|
||||
$license->created_by = auth()->id();
|
||||
}
|
||||
|
||||
// This sets an attribute on the Loggable trait for the action log
|
||||
|
|
|
@ -51,6 +51,7 @@ class LocationImporter extends ItemImporter
|
|||
} else {
|
||||
$this->log('No Matching Location, Create a new one');
|
||||
$location = new Location;
|
||||
$location->created_by = auth()->id();
|
||||
}
|
||||
|
||||
// Pull the records from the CSV to determine their values
|
||||
|
@ -65,7 +66,6 @@ class LocationImporter extends ItemImporter
|
|||
$this->item['ldap_ou'] = trim($this->findCsvMatch($row, 'ldap_ou'));
|
||||
$this->item['manager'] = trim($this->findCsvMatch($row, 'manager'));
|
||||
$this->item['manager_username'] = trim($this->findCsvMatch($row, 'manager_username'));
|
||||
$this->item['created_by'] = auth()->id();
|
||||
|
||||
if ($this->findCsvMatch($row, 'parent_location')) {
|
||||
$this->item['parent_id'] = $this->createOrFetchLocation(trim($this->findCsvMatch($row, 'parent_location')));
|
||||
|
|
|
@ -114,6 +114,7 @@ class UserImporter extends ItemImporter
|
|||
|
||||
$this->log('No matching user, creating one');
|
||||
$user = new User();
|
||||
$user->created_by = auth()->id();
|
||||
$user->fill($this->sanitizeItemForStoring($user));
|
||||
|
||||
if ($user->save()) {
|
||||
|
|
|
@ -73,6 +73,9 @@ class Importer extends Component
|
|||
case 'asset':
|
||||
$results = $this->assets_fields;
|
||||
break;
|
||||
case 'assetModel':
|
||||
$results = $this->assetmodels_fields;
|
||||
break;
|
||||
case 'accessory':
|
||||
$results = $this->accessories_fields;
|
||||
break;
|
||||
|
@ -82,6 +85,9 @@ class Importer extends Component
|
|||
case 'component':
|
||||
$results = $this->components_fields;
|
||||
break;
|
||||
case 'consumable':
|
||||
$results = $this->consumables_fields;
|
||||
break;
|
||||
case 'license':
|
||||
$results = $this->licenses_fields;
|
||||
break;
|
||||
|
@ -91,10 +97,14 @@ class Importer extends Component
|
|||
case 'location':
|
||||
$results = $this->locations_fields;
|
||||
break;
|
||||
case 'user':
|
||||
$results = $this->users_fields;
|
||||
break;
|
||||
default:
|
||||
$results = [];
|
||||
}
|
||||
asort($results, SORT_FLAG_CASE | SORT_STRING);
|
||||
|
||||
if ($type == "asset") {
|
||||
// add Custom Fields after a horizontal line
|
||||
$results['-'] = "———" . trans('admin/custom_fields/general.custom_fields') . "———’";
|
||||
|
@ -107,6 +117,7 @@ class Importer extends Component
|
|||
|
||||
public function updatingTypeOfImport($type)
|
||||
{
|
||||
|
||||
// go through each header, find a matching field to try and map it to.
|
||||
foreach ($this->headerRow as $i => $header) {
|
||||
// do we have something mapped already?
|
||||
|
@ -152,13 +163,14 @@ class Importer extends Component
|
|||
{
|
||||
$this->authorize('import');
|
||||
$this->importTypes = [
|
||||
'asset' => trans('general.assets'),
|
||||
'accessory' => trans('general.accessories'),
|
||||
'consumable' => trans('general.consumables'),
|
||||
'component' => trans('general.components'),
|
||||
'license' => trans('general.licenses'),
|
||||
'user' => trans('general.users'),
|
||||
'location' => trans('general.locations'),
|
||||
'accessory' => trans('general.accessories'),
|
||||
'asset' => trans('general.assets'),
|
||||
'assetModel' => trans('general.asset_models'),
|
||||
'component' => trans('general.components'),
|
||||
'consumable' => trans('general.consumables'),
|
||||
'license' => trans('general.licenses'),
|
||||
'location' => trans('general.locations'),
|
||||
'user' => trans('general.users'),
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -331,6 +343,19 @@ class Importer extends Component
|
|||
'parent_location' => trans('admin/locations/table.parent'),
|
||||
];
|
||||
|
||||
$this->assetmodels_fields = [
|
||||
'item_name' => trans('general.item_name_var', ['item' => trans('general.asset_model')]),
|
||||
'category' => trans('general.category'),
|
||||
'manufacturer' => trans('general.manufacturer'),
|
||||
'model_number' => trans('general.model_no'),
|
||||
'notes' => trans('general.item_notes', ['item' => trans('admin/hardware/form.model')]),
|
||||
'min_amt' => trans('mail.min_QTY'),
|
||||
'fieldset' => trans('admin/models/general.fieldset'),
|
||||
'eol' => trans('general.eol'),
|
||||
'requestable' => trans('admin/models/general.requestable'),
|
||||
|
||||
];
|
||||
|
||||
// "real fieldnames" to a list of aliases for that field
|
||||
$this->aliases_fields = [
|
||||
'item_name' =>
|
||||
|
@ -359,6 +384,23 @@ class Importer extends Component
|
|||
'eol date',
|
||||
'asset eol date',
|
||||
],
|
||||
'eol' =>
|
||||
[
|
||||
'eol',
|
||||
'EOL',
|
||||
'eol months',
|
||||
],
|
||||
'depreciation' =>
|
||||
[
|
||||
'Depreciation',
|
||||
'depreciation',
|
||||
],
|
||||
'requestable' =>
|
||||
[
|
||||
'requestable',
|
||||
'Requestable',
|
||||
],
|
||||
|
||||
'gravatar' =>
|
||||
[
|
||||
'gravatar',
|
||||
|
@ -503,7 +545,6 @@ class Importer extends Component
|
|||
if (!$this->activeFile) {
|
||||
$this->message = trans('admin/hardware/message.import.file_missing');
|
||||
$this->message_type = 'danger';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -518,6 +559,8 @@ class Importer extends Component
|
|||
$this->field_map[] = null; // re-inject the 'nulls' if a file was imported with some 'Do Not Import' settings
|
||||
}
|
||||
}
|
||||
|
||||
$this->file_id = $id;
|
||||
$this->import_errors = null;
|
||||
$this->statusText = null;
|
||||
|
||||
|
|
|
@ -113,7 +113,9 @@ class SlackSettingsForm extends Component
|
|||
if($this->webhook_selected == 'microsoft' || $this->webhook_selected == 'google'){
|
||||
$this->webhook_channel = '#NA';
|
||||
}
|
||||
|
||||
}
|
||||
public function updatedwebhookEndpoint() {
|
||||
$this->teams_webhook_deprecated = !Str::contains($this->webhook_endpoint, 'workflows');
|
||||
}
|
||||
public function updatedwebhookEndpoint() {
|
||||
$this->teams_webhook_deprecated = !Str::contains($this->webhook_endpoint, 'workflows');
|
||||
|
|
|
@ -68,6 +68,7 @@ class AssetModel extends SnipeModel
|
|||
'model_number',
|
||||
'name',
|
||||
'notes',
|
||||
'requestable',
|
||||
];
|
||||
|
||||
use Searchable;
|
||||
|
@ -328,4 +329,14 @@ class AssetModel extends SnipeModel
|
|||
{
|
||||
return $query->leftJoin('custom_fieldsets', 'models.fieldset_id', '=', 'custom_fieldsets.id')->orderBy('custom_fieldsets.name', $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder scope to order on created_by name
|
||||
*
|
||||
*/
|
||||
public function scopeOrderByCreatedByName($query, $order)
|
||||
{
|
||||
return $query->leftJoin('users as admin_sort', 'models.created_by', '=', 'admin_sort.id')->select('models.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,4 +14,16 @@ class Import extends Model
|
|||
'first_row' => 'array',
|
||||
'field_map' => 'json',
|
||||
];
|
||||
|
||||
/**
|
||||
* Establishes the license -> admin user relationship
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since [v2.0]
|
||||
* @return \Illuminate\Database\Eloquent\Relations\Relation
|
||||
*/
|
||||
public function adminuser()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,4 +143,26 @@ class ImportFactory extends Factory
|
|||
return $attributes;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create an asset model import type.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function assetmodel()
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
$fileBuilder = Importing\AssetModelsImportFileBuilder::new();
|
||||
|
||||
$attributes['name'] = "{$attributes['name']} Asset Model";
|
||||
$attributes['import_type'] = 'assetModel';
|
||||
$attributes['header_row'] = $fileBuilder->toCsv()[0];
|
||||
$attributes['first_row'] = $fileBuilder->firstRow();
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -560,7 +560,7 @@ return [
|
|||
'something_went_wrong' => 'Something went wrong with your request.',
|
||||
'close' => 'Close',
|
||||
'expires' => 'Expires',
|
||||
'map_fields'=> 'Map :item_type Field',
|
||||
'map_fields'=> 'Map :item_type Fields',
|
||||
'remaining_var' => ':count Remaining',
|
||||
'label' => 'Label',
|
||||
'import_asset_tag_exists' => 'An asset with the asset tag :asset_tag already exists and an update was not requested. No change was made.',
|
||||
|
|
|
@ -105,13 +105,17 @@
|
|||
class="col-md-12 table table-striped snipe-table">
|
||||
|
||||
<tr>
|
||||
<th class="col-md-6">
|
||||
<th>
|
||||
{{ trans('general.file_name') }}
|
||||
</th>
|
||||
<th class="col-md-3">
|
||||
<th>
|
||||
{{ trans('general.created_at') }}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<th>
|
||||
{{ trans('general.created_by') }}
|
||||
</th>
|
||||
|
||||
<th>
|
||||
{{ trans('general.filesize') }}
|
||||
</th>
|
||||
<th class="col-md-1 text-right">
|
||||
|
@ -122,9 +126,10 @@
|
|||
@foreach($this->files as $currentFile)
|
||||
|
||||
<tr style="{{ ($this->activeFile && ($currentFile->id == $this->activeFile->id)) ? 'font-weight: bold' : '' }}" class="{{ ($this->activeFile && ($currentFile->id == $this->activeFile->id)) ? 'warning' : '' }}">
|
||||
<td class="col-md-6">{{ $currentFile->file_path }}</td>
|
||||
<td class="col-md-3">{{ Helper::getFormattedDateObject($currentFile->created_at, 'datetime', false) }}</td>
|
||||
<td class="col-md-1">{{ Helper::formatFilesizeUnits($currentFile->filesize) }}</td>
|
||||
<td>{{ $currentFile->file_path }}</td>
|
||||
<td>{{ Helper::getFormattedDateObject($currentFile->created_at, 'datetime', false) }}</td>
|
||||
<td>{{ ($currentFile->adminuser) ? $currentFile->adminuser->present()->fullName : '--'}}</td>
|
||||
<td>{{ Helper::formatFilesizeUnits($currentFile->filesize) }}</td>
|
||||
<td class="col-md-1 text-right" style="white-space: nowrap;">
|
||||
<button class="btn btn-sm btn-info" wire:click="selectFile({{ $currentFile->id }})" data-tooltip="true" title="{{ trans('general.import_this_file') }}">
|
||||
<i class="fa-solid fa-list-check" aria-hidden="true"></i>
|
||||
|
@ -147,7 +152,7 @@
|
|||
{{ trans('general.import_type') }}
|
||||
</label>
|
||||
|
||||
<div class="col-md-9 col-xs-12" wire:ignore>
|
||||
<div class="col-md-9 col-xs-12">
|
||||
{{ Form::select('typeOfImport', $importTypes, $typeOfImport, [
|
||||
'id' => 'import_type',
|
||||
'class' => 'livewire-select2',
|
||||
|
@ -170,20 +175,25 @@
|
|||
<input type="checkbox" name="update" data-livewire-component="{{ $this->getId() }}" wire:model.live="update">
|
||||
{{ trans('general.update_existing_values') }}
|
||||
</label>
|
||||
|
||||
@if ($typeOfImport === 'asset' && $snipeSettings->auto_increment_assets == 1 && $update)
|
||||
<p class="help-block">
|
||||
{{ trans('general.auto_incrementing_asset_tags_enabled_so_now_assets_will_be_created') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
@if (($typeOfImport != 'location' && $typeOfImport!= 'assetModel') && ($typeOfImport!=''))
|
||||
<label class="form-control">
|
||||
<input type="checkbox" name="send_welcome" data-livewire-component="{{ $this->getId() }}" wire:model.live="send_welcome">
|
||||
{{ trans('general.send_welcome_email_to_users') }}
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<label class="form-control">
|
||||
<input type="checkbox" name="run_backup" data-livewire-component="{{ $this->getId() }}" wire:model.live="run_backup">
|
||||
{{ trans('general.back_before_importing') }}
|
||||
{{ trans('general.back_before_importing') }}
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
@ -225,7 +235,7 @@
|
|||
<div class="form-group col-md-12" wire:key="header-row-{{ $index }}">
|
||||
|
||||
<label for="field_map.{{ $index }}" class="col-md-3 control-label text-right">{{ $header }}</label>
|
||||
<div class="col-md-4" wire:ignore>
|
||||
<div class="col-md-4">
|
||||
|
||||
{{ Form::select('field_map.'.$index, $columnOptions[$typeOfImport], @$field_map[$index],
|
||||
[
|
||||
|
|
21
sample_csvs/models-sample.csv
Normal file
21
sample_csvs/models-sample.csv
Normal file
|
@ -0,0 +1,21 @@
|
|||
Name,Category,Manufacturer,Model Notes,Model Number,Fieldset,Requestable,EOL,Min Qty
|
||||
Test Model 2,Laptops,"Botsford, Boyle and Herzog",ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae mauris viverra diam vitae quam suspendisse potenti nullam,9351IS25A51,Laptops and Desktops,Y,36,50
|
||||
Test Model 3,Laptops,Pollich LLC,,9929FR08W85,Laptops and Desktops,Y,36,30
|
||||
Test Model 5,Laptops,Berge Inc,turpis adipiscing lorem vitae mattis nibh ligula nec sem duis aliquam convallis nunc proin at turpis a pede,0910VB28Q61,Laptops and Desktops,Y,36,0
|
||||
Test Model 6,Laptops,"Heaney, Altenwerth and Emmerich",,7375EM02N97,Laptops and Desktops,Y,36,100
|
||||
Test Model 7,Desktops,"Romaguera, Goldner and Crooks",Test Updated Text,,Laptops and Desktops,Y,36,100
|
||||
Test Model 8,Laptops,Watsica LLC,sapien urna pretium nisl ut volutpat sapien arcu sed augue aliquam erat volutpat in,,Laptops and Desktops,Y,36,100
|
||||
Test Model 9,Laptops,"Fritsch, Sauer and Conn",orci luctus et ultrices posuere cubilia curae duis faucibus accumsan odio curabitur,,Laptops and Desktops,Y,36,100
|
||||
Test Model 10,Laptops,"Upton, Feil and Jast",velit vivamus vel nulla eget eros elementum pellentesque quisque porta volutpat,,Laptops and Desktops,Y,36,100
|
||||
Test Model 11,Laptops,Berge Inc,sed nisl nunc rhoncus dui vel sem sed sagittis nam congue risus semper porta volutpat quam pede lobortis ligula sit,,Laptops and Desktops,Y,36,100
|
||||
Test Model 12,Laptops,"Kutch, Johnson and Olson",curae mauris viverra diam vitae quam suspendisse potenti nullam porttitor lacus at turpis donec posuere metus vitae ipsum aliquam non,,Laptops and Desktops,Y,36,100
|
||||
Test Model 13,Laptops,Mosciski Inc,molestie hendrerit at vulputate vitae nisl aenean lectus pellentesque eget nunc donec quis orci,,Laptops and Desktops,Y,36,100
|
||||
Test Model 14,Laptops,Mosciski Inc,egestas metus aenean fermentum donec ut mauris eget massa tempor convallis nulla neque libero convallis eget eleifend,,Laptops and Desktops,N,36,100
|
||||
Test Model 15,Laptops,"Upton, Feil and Jast",,,Laptops and Desktops,N,36,100
|
||||
Test Model 16,Laptops,"Romaguera, Goldner and Crooks",dui luctus rutrum nulla tellus in sagittis dui vel nisl duis ac nibh fusce,2315CN41G71,Laptops and Desktops,N,36,100
|
||||
Test Model 17,Laptops,Abernathy-Stamm,maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus,6080UE59E09,Laptops and Desktops,N,36,100
|
||||
Test Model 18,Laptops,Mosciski Inc,,5505YF23M46,Laptops and Desktops,N,36,100
|
||||
Test Model 19,Laptops,Walker-Towne,,8673QP30R80,Laptops and Desktops,Y,36,100
|
||||
Test Model 20,Mobile Phones,"Heaney, Altenwerth and Emmerich",nisl ut volutpat sapien arcu sed augue aliquam erat volutpat in congue etiam justo etiam pretium,9088XV67Q94,Mobile Devices,Y,12,100
|
||||
Test One,Mobile Phones,Okuneva Group,quis libero nullam sit amet turpis elementum ligula vehicula consequat morbi a ipsum integer a nibh in quis,,Mobile Devices,Y,12,100
|
||||
Final Test,Mobile Phones,Walker-Towne,"Sphinx of black quartz, judge my vow",,Mobile Devices,Y,36,15
|
|
140
tests/Feature/Importing/Api/ImportAssetModelsTest.php
Normal file
140
tests/Feature/Importing/Api/ImportAssetModelsTest.php
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Importing\Api;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\User;
|
||||
use App\Models\Import;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Concerns\TestsPermissionsRequirement;
|
||||
use Tests\Support\Importing\CleansUpImportFiles;
|
||||
use Tests\Support\Importing\AssetModelsImportFileBuilder as ImportFileBuilder;
|
||||
|
||||
class ImportAssetModelsTest extends ImportDataTestCase implements TestsPermissionsRequirement
|
||||
{
|
||||
use CleansUpImportFiles;
|
||||
use WithFaker;
|
||||
|
||||
protected function importFileResponse(array $parameters = []): TestResponse
|
||||
{
|
||||
if (!array_key_exists('import-type', $parameters)) {
|
||||
$parameters['import-type'] = 'assetModel';
|
||||
}
|
||||
|
||||
return parent::importFileResponse($parameters);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function testRequiresPermission()
|
||||
{
|
||||
$this->actingAsForApi(User::factory()->create());
|
||||
|
||||
$this->importFileResponse(['import' => 44])->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function importAssetModels(): void
|
||||
{
|
||||
$importFileBuilder = ImportFileBuilder::new();
|
||||
$row = $importFileBuilder->firstRow();
|
||||
$import = Import::factory()->assetmodel()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||
$this->importFileResponse(['import' => $import->id, 'send-welcome' => 0])
|
||||
->assertOk()
|
||||
->assertExactJson([
|
||||
'payload' => null,
|
||||
'status' => 'success',
|
||||
'messages' => ['redirect_url' => route('models.index')]
|
||||
]);
|
||||
|
||||
$newAssetModel = AssetModel::query()
|
||||
->with(['category'])
|
||||
->where('name', $row['name'])
|
||||
->sole();
|
||||
|
||||
$this->assertEquals($row['name'], $newAssetModel->name);
|
||||
$this->assertEquals($row['model_number'], $newAssetModel->model_number);
|
||||
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
|
||||
{
|
||||
$row = ImportFileBuilder::new()->definition();
|
||||
$row['unknownColumnInCsvFile'] = 'foo';
|
||||
|
||||
$importFileBuilder = new ImportFileBuilder([$row]);
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||
|
||||
$import = Import::factory()->assetmodel()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
||||
|
||||
$this->importFileResponse(['import' => $import->id])->assertOk();
|
||||
}
|
||||
|
||||
|
||||
#[Test]
|
||||
public function whenRequiredColumnsAreMissingInImportFile(): void
|
||||
{
|
||||
$importFileBuilder = ImportFileBuilder::new(['name' => '']);
|
||||
$import = Import::factory()->assetmodel()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||
|
||||
$this->importFileResponse(['import' => $import->id])
|
||||
->assertInternalServerError()
|
||||
->assertExactJson([
|
||||
'status' => 'import-errors',
|
||||
'payload' => null,
|
||||
'messages' => [
|
||||
'' => [
|
||||
'name' => [
|
||||
'name' =>
|
||||
['The name field is required.'],
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$newAssetModels = AssetModel::query()
|
||||
->where('name', $importFileBuilder->firstRow()['name'])
|
||||
->get();
|
||||
|
||||
$this->assertCount(0, $newAssetModels);
|
||||
}
|
||||
|
||||
|
||||
#[Test]
|
||||
public function updateAssetModelFromImport(): void
|
||||
{
|
||||
$assetmodel = AssetModel::factory()->create()->refresh();
|
||||
$category = Category::find($assetmodel->category->name);
|
||||
$importFileBuilder = ImportFileBuilder::new(['name' => $assetmodel->name, 'model_number' => Str::random(), 'category' => $category]);
|
||||
|
||||
$row = $importFileBuilder->firstRow();
|
||||
$import = Import::factory()->assetmodel()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
||||
|
||||
$this->actingAsForApi(User::factory()->superuser()->create());
|
||||
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
|
||||
|
||||
$updatedAssetmodel = AssetModel::query()->find($assetmodel->id);
|
||||
$updatedAttributes = [
|
||||
'name',
|
||||
'model_number'
|
||||
];
|
||||
|
||||
$this->assertEquals($row['model_number'], $updatedAssetmodel->model_number);
|
||||
|
||||
$this->assertEquals(
|
||||
Arr::except($assetmodel->attributesToArray(), $updatedAttributes),
|
||||
Arr::except($updatedAssetmodel->attributesToArray(), $updatedAttributes),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -105,7 +105,6 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
|
|||
$this->assertEquals(0, $newUser->vip);
|
||||
$this->assertEquals(0, $newUser->enable_sounds);
|
||||
$this->assertEquals(0, $newUser->enable_confetti);
|
||||
$this->assertNull($newUser->created_by);
|
||||
$this->assertNull($newUser->start_date);
|
||||
$this->assertNull($newUser->end_date);
|
||||
$this->assertNull($newUser->scim_externalid);
|
||||
|
@ -322,7 +321,6 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
|
|||
$this->assertEquals(0, $newUser->vip);
|
||||
$this->assertEquals(0, $newUser->enable_sounds);
|
||||
$this->assertEquals(0, $newUser->enable_confetti);
|
||||
$this->assertNull($newUser->created_by);
|
||||
$this->assertNull($newUser->start_date);
|
||||
$this->assertNull($newUser->end_date);
|
||||
$this->assertNull($newUser->scim_externalid);
|
||||
|
|
57
tests/Support/Importing/AssetModelsImportFileBuilder.php
Normal file
57
tests/Support/Importing/AssetModelsImportFileBuilder.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support\Importing;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Build a users import file at runtime for testing.
|
||||
*
|
||||
* @template Row of array{
|
||||
* name?: string,
|
||||
* manufacturer?: string,
|
||||
* category?: string,
|
||||
* model_number?: string,
|
||||
* requestable?: int,
|
||||
* }
|
||||
*
|
||||
* @extends FileBuilder<Row>
|
||||
*/
|
||||
class AssetModelsImportFileBuilder extends FileBuilder
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function getDictionary(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Name',
|
||||
'category' => 'Category',
|
||||
'manufacturer' => 'Manufacturer',
|
||||
'model_number' => 'Model Number',
|
||||
'fieldset' => 'Fieldset',
|
||||
'eol' => 'EOL',
|
||||
'min_amt' => 'Min Amount',
|
||||
'notes' => 'Notes',
|
||||
'requestable' => 'Requestable',
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$faker = fake();
|
||||
|
||||
return [
|
||||
'name' => $faker->catchPhrase,
|
||||
'category' => Str::random(),
|
||||
'model_number' => $faker->creditCardNumber(),
|
||||
'notes' => 'Created by demo seeder',
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue