Merge pull request #15802 from snipe/features/import_models

Ability to import asset models (separate from assets)
This commit is contained in:
snipe 2024-11-13 20:37:31 +00:00 committed by GitHub
commit 3c08760aeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 567 additions and 42 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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')

View file

@ -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'),

View file

@ -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));

View 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;
}
}

View file

@ -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,

View file

@ -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"));

View file

@ -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
*

View file

@ -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');

View file

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

View file

@ -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')));

View file

@ -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()) {

View file

@ -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;

View file

@ -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');

View file

@ -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);
}
}

View file

@ -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');
}
}

View file

@ -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;
});
}
}

View file

@ -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.',

View file

@ -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],
[

View 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
1 Name Category Manufacturer Model Notes Model Number Fieldset Requestable EOL Min Qty
2 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
3 Test Model 3 Laptops Pollich LLC 9929FR08W85 Laptops and Desktops Y 36 30
4 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
5 Test Model 6 Laptops Heaney, Altenwerth and Emmerich 7375EM02N97 Laptops and Desktops Y 36 100
6 Test Model 7 Desktops Romaguera, Goldner and Crooks Test Updated Text Laptops and Desktops Y 36 100
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 Test Model 15 Laptops Upton, Feil and Jast Laptops and Desktops N 36 100
15 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
16 Test Model 17 Laptops Abernathy-Stamm maecenas pulvinar lobortis est phasellus sit amet erat nulla tempus 6080UE59E09 Laptops and Desktops N 36 100
17 Test Model 18 Laptops Mosciski Inc 5505YF23M46 Laptops and Desktops N 36 100
18 Test Model 19 Laptops Walker-Towne 8673QP30R80 Laptops and Desktops Y 36 100
19 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
20 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
21 Final Test Mobile Phones Walker-Towne Sphinx of black quartz, judge my vow Mobile Devices Y 36 15

View 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),
);
}
}

View file

@ -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);

View 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',
];
}
}